Модуль:Календарь

Материал из свободной русской энциклопедии «Традиция»
Перейти к навигации Перейти к поиску

Модуль для календарных расчётов.

Функции:

Пасхалия 
Функция для расчёта Пасхи и привязанных к ней праздников
век 
Функция для расчёта века, которому принадлежит год
десятилетие 
Функция для расчёта десятилетия, которому принадлежит год
пасхалия 
Функция для расчёта Пасхи и привязанных к ней праздников

Навигатор

2020

2010-е

XX век

753 до н. э.

VIII век до н. э.


--[[
	Dependencies
--]]
local locale = require 'Модуль:Локаль'
local roman, nonArabic, NAG = locale.roman, locale.nonArabic, locale.nonArabicGrammar
local function deromanise (roman)
	return nonArabic (roman, 'roman')
end	-- local function deromanise (roman)
local floor = math.floor

local m = {}
 
local CalendarData = {
    defaultMethod = 2,			-- default method of Easter date calculation when Easter type is not given
    defaultFormat = "d.m.Y",	-- default date output format
    noFormat      = "none",		-- prevent from final date formatting
    defaultOffset = 0,			-- the Easter date
    minimumOffset = -63,		-- Septuagesima
    maximumOffset = 68,			-- Feast of the Sacred Heart

    epochs			= {
    	[1]		= {
    		API		= 'год' -- year name.
    	},
    	[10]	= {
    		suffix	= '-е',
    		API		= 'десятилетие' -- decade name.
    	},
    	[100]	= {
    		suffix	= ' век',
    		style	= 'roman',
    		from1	= true,
			API		= 'век'
		},
    	[1000]	= {
    		suffix	= ' тысячелетие',
    		from1	= true,
			API		= 'тысячелетие'
		}
	},	-- epochs
    BC				= 'до н. э.',

    -- API:
    -- Easter:
    apiEaster			= 'пасхалия',	-- public function name
    argYear				= 1,			-- index or name of the argument with year
    argEasterMethod	= 'пасхалия',		-- index or name of the argument with calculation method
    argEasterOffset		= 'день',		-- index or name of the argument with offset in days relative to the calculated Easter Sunday
    argEasterFormat		= 'формат',		-- index or name of the argument with date output format (#time style)
    -- Decades and centuries:
    argYearOffset		= 2,
    argYearNoEra		= 'без эры',
    apiNavigator		= 'годы',
    -- Navigator:
    argEpoch			= 1,
 
    -- errors
    errorMissingYear       = 'Не указан год',
    errorMissingEpoch      = 'Не указана эпоха',
    errorInvalidYear       = "Аргумент «год» неверен: '%s'",
    errorInvalidEpoch      = "Аргумент «эпоха» неверен: '%s'",    
    errorNo0thYear         = "Нулевого года не бывает",
    errorInvalidOffset     = "Аргумент «дни» ошибочен: '%s'",
    errorInvalidMethod     = "Аргумент «пасхалия» ошибочен: '%s'",
    errorYearOutOfRange    = "Пасха рассчитывается только с 326 по 4099 годы; указан год %d",
    errorIncorrectMethod   = "Западная и православная пасхалии отделяются от юлианской только с 1583; указан год %d",
    errorUnknownMethod     = "Неизвестный метод: %d",
    errorInvalidYearOffset = "Аргумент «сдвиг» неверен: '%s'",
 
    methods = {
        ["юлианская"]     = 1,
        ["восточная"]     = 2,
        ["православная"]  = 2, -- alias for Eastern
        ["западная"]      = 3,
        ["римская"]       = 3, -- alias for Roman
    },
 
    relativeDates = {
        ["Семидесятница"]            = -63,
        ["Sexagesima"]               = -56,
        ["Fat Thursday"]             = -52,
        ["Прощёное воскресенье"]     = -49,
        ["Чистый понедельник"]       = -48,
        ["Пепельная среда"]          = -46,
        ["Вербное воскресенье"]      =  -7,
        ["Чистый четверг"]           =  -3,
        ["Великий четверг"]          =  -3,
        ["Страстная пятница"]        =  -2,
        ["Великая суббота"]          =  -1, 
        ["Пасха"]                    =   0,
        ["Пасхальный понедельник"]   =   1,
        ["Вознесение"]               =  39,
        ["Отдание Пасхи"]            =  39,
        ["Пятидесятница"]            =  49,
        ["Corpus Christi"]           =  60,
    },
}
 
local function formatCalendarError (message, ...)
    if select('#', ... ) > 0 then
        message = string.format(message, ...)
    end
    return "<span class=\"error\">" .. message .. "</span>"
end
 
local sub, gsub, len = mw.ustring.sub, mw.ustring.gsub, mw.ustring.len
local function loadYear (year)
    if not year then
        return false, formatCalendarError (CalendarData.errorMissingYear)
    end
    -- Process BC:
    year = gsub (year, '%s', '')
    if sub (year, -6) == gsub (CalendarData.BC, '%s', '') then
    	year = '-' .. sub (year, 1, len (year) - 6)
    end
    local result = tonumber(year)
    if not result or math.floor(result) ~= result then
        return false, formatCalendarError (CalendarData.errorInvalidYear, year)
    end
    if result == 0 then
        return false, formatCalendarError (CalendarData.errorNo0thYear, year)
    end

    return true, result
end

local function loadEasterMethod(method, year)
    local result = CalendarData.defaultMethod
    if method then
        result = CalendarData.methods[method]
        if not result then
            return false, formatCalendarError (CalendarData.errorInvalidMethod, method)
        end
    end
 
    if year < 1583 then
        result = 1
    end
 
    return true, result
end
 
local function loadEasterOffset(day)
    if not day then
        return true, ""
    end
 
    local data = CalendarData.relativeDates
    local offset = tonumber(day)
    if not offset then
        offset = data[day]
    end
    if not offset or offset ~= math.floor(offset) or offset < CalendarData.minimumOffset or offset > CalendarData.maximumOffset then
        return false, formatCalendarError (CalendarData.errorInvalidOffset, day)
    end

    if offset < -1 then
        return true, string.format(" %d days", offset)
    elseif offset == -1 then
        return true, " -1 day"
    elseif offset == 0 then
        return true, ""
    elseif offset == 1 then
        return true, " +1 day"
    else -- if offset > 1 then
        return true, string.format(" +%d days", offset)
    end
end
 
local function loadEasterFormat(fmt)
    if fmt == CalendarData.noFormat then
        return true, nil
    elseif not fmt then
        return true, CalendarData.defaultFormat
    else
        return true, fmt
    end
end
 
--[[
 PURPOSE:     This function returns Easter Sunday day and month
              for a specified year and method.
 
 INPUTS:      Year   - Any year between 326 and 4099.
              Method - 1 = the original calculation based on the
                           Julian calendar
                       2 = the original calculation, with the
                           Julian date converted to the
                           equivalent Gregorian calendar
                       3 = the revised calculation based on the
                           Gregorian calendar
 
 OUTPUTS:     None.
 
 RETURNS:     0, error message - Error; invalid arguments
              month, day       - month and day of the Sunday
 
 NOTES:
              The code is translated from DN OSP 6.4.0 sources.
              The roots of the code might be found in
              http://www.gmarts.org/index.php?go=415
 
 ORIGINAL NOTES:
 
              This algorithm is an arithmetic interpretation
              of the 3 step Easter Dating Method developed
              by Ron Mallen 1985, as a vast improvement on
              the method described in the Common Prayer Book
 
              Published Australian Almanac 1988
              Refer to this publication, or the Canberra Library
              for a clear understanding of the method used
 
              Because this algorithm is a direct translation of
              the official tables, it can be easily proved to be
              100% correct
 
              It's free! Please do not modify code or comments!
]]
local function calculateEasterDate(year, method)
    if year < 326 or year > 4099 then
        -- Easter dates are valid for years between 326 and 4099
        return 0, formatCalendarError  (CalendarData.errorYearOutOfRange, year)
    end
    if year < 1583 and method ~= 1 then
        -- Western or Orthodox Easter is valid since 1583
        return 0, formatCalendarError  (CalendarData.errorIncorrectMethod, year)
    end
 
    -- intermediate result
    local firstDig = math.floor(year / 100)
    local remain19 = year % 19
    local temp = 0
    -- table A to E results
    local tA = 0
    local tB = 0
    local tC = 0
    local tD = 0
    local tE = 0
     -- Easter Sunday day
    local d = 0
 
    if method == 1 or method == 2 then
        -- calculate PFM date
        tA   = ((225 - 11 * remain19) % 30) + 21
        -- find the next Sunday
        tB   = (tA - 19) % 7
        tC   = (40 - firstDig) % 7
        temp = year % 100
        tD   = (temp + math.floor(temp / 4)) % 7
        tE   = ((20 - tB - tC - tD) % 7) + 1
        d    = tA + tE
        if method == 2 then
            -- convert Julian to Gregorian date
            -- 10 days were skipped in the Gregorian calendar from 5-14 Oct 1582
            temp = 10
            -- only 1 in every 4 century years are leap years in the Gregorian
            -- calendar (every century is a leap year in the Julian calendar)
            if year > 1600 then
                temp = temp + firstDig - 16 - math.floor((firstDig - 16) / 4)
            end
            d = d + temp
        end
    elseif method == 3 then
        -- calculate PFM date
        temp = math.floor((firstDig - 15) / 2)  + 202 - 11 * remain19
        if firstDig > 26 then
            temp = temp - 1
        end
        if firstDig > 38 then
            temp = temp - 1
        end
        if firstDig == 21 or firstDig == 24 or firstDig == 25 or firstDig == 33 or firstDig == 36 or firstDig == 37 then
            temp = temp - 1
        end
        temp = temp % 30
        tA   = temp + 21
        if temp == 29 then
            tA = tA - 1
        end
        if temp == 28 and remain19 > 10 then
            tA = tA - 1
        end
        -- find the next Sunday
        tB   = (tA - 19) % 7
        tC   = (40 - firstDig) % 4
        if tC == 3 then
            tC = tC + 1
        end
        if tC > 1 then
            tC = tC + 1
        end
        temp = year % 100
        tD   = (temp + math.floor(temp / 4)) % 7
        tE   = ((20 - tB - tC - tD) % 7) + 1
        d    = tA + tE
    else
        -- Unknown method
        return 0, formatEeasteError(CalendarData.errorUnknownMethod, method)
    end
    if d > 61 then
        -- when the oryginal calculation os converted to the Gregorian
        -- calendar, Easter Sunday can occur in May
        return 5, d - 61
    elseif d > 31 then
        return 4, d - 31
    else
        return 3, d
    end
end
 
local function Easter (args)
    local ok
    local year
    ok, year = loadYear (args[CalendarData.argYear])
    if not ok then
        return year
    end
 
    local method
    ok, method = loadEasterMethod (args[CalendarData.argEasterMethod], year)
    if not ok then
        return method
    end
 
    local offset
    ok, offset = loadEasterOffset (args[CalendarData.argEasterOffset])
    if not ok then
        return offset
    end
 
    local format
    ok, format = loadEasterFormat (args[CalendarData.argEasterFormat])
    if not ok then
        return format
    end
 
    local month, day = calculateEasterDate (year, method)
    if month == 0 then
        return day
    end
 
    local result = string.format("%04d-%02d-%02d%s", year, month, day, offset)
    if format then
        result = mw.language.getContentLanguage():formatDate(format, result)
    end
 
    return result
end
 
-- Общий синтаксис, с "пасхалия":
m [CalendarData.apiEaster] = function (frame)
    return Easter (frame.args)
end

-- Сокращённый синтаксис, без "пасхалия":
for holiday, _ in pairs (CalendarData.relativeDates) do
    m [holiday] = function (frame)
        local args = frame.args
        args [CalendarData.argEasterOffset] = holiday
        return Easter (args)
    end
end

--[[
	Period names:
--]]
local function loadYearOffset (offset)
	if not offset then
		return true, 0
    end
	local result = tonumber (offset)
    if not result or math.floor (result) ~= result then
		return false, formatCalendarError (CalendarData.errorInvalidYearOffset, offset)
	end
	return true, result
end	-- local function loadYearOffset (offset)

local function boundaries (year, length)
	local digit = year > 0 and 1 or 0
	local start = (year - digit) - (year - digit) % length + digit
	return start, start + length - 1
end	-- local function boundaries (year, length)
m.b = boundaries

local function number (year, length, from1, style)
	local serialiser = style and locale [style] or tostring
	local no = from1 and floor (year / length) + 1 or year - year % length
	return serialiser (no)
end	-- local function number (year, length, from1, style)
m.nn = number

local abs = math.abs
local function year2epoch (year, length, suffix, era)
	local start, finish = boundaries (year, length)
	local name = abs (length > 1 and (year > 0 and start - 1 or finish + 1) or start)
    local epoch = CalendarData.epochs [length]
    return number (name, length, epoch.from1, epoch.style)
    	.. (suffix and epoch.suffix or '')
    	.. (era and year < 0 and ' ' .. CalendarData.BC or '')
end	-- local function year2epoch (year, length, era)

local function wrapEpoch (func)
	return function (frame)
		local args = frame.args
		local ok, year, offset, era
		ok, year = loadYear (args [CalendarData.argYear])
		if not ok then
			return year
		end
		ok, offset = loadYearOffset (args [CalendarData.argYearOffset])
		if not ok then
			return offset
		end
		era =  not (args [CalendarData.argYearNoEra] and args [CalendarData.argYearNoEra] ~= '')
		return func (year + offset, era)
	end
end	-- local function wrapEpoch (func)

for length, epoch in pairs (CalendarData.epochs) do
	m [epoch.API] = wrapEpoch (function (year, era)
		return year2epoch (year, length, epoch.suffix, era)
	end)
end
m.d = function (year, era)
	return year2epoch (year, 10, true, era)
end

-- An iterator over epochs from greater to lesser:
local function epochs (reverse)
	local lengths = {}
	for length, _ in pairs (CalendarData.epochs) do
		lengths [#lengths + 1] = length
	end
	table.sort (lengths, reverse and function (a, b) return a > b end or nil)
	local key = 0
	return function ()
		key = key + 1
		if key <= #lengths then
			return lengths [key], CalendarData.epochs [lengths [key]]
		end
	end
end	-- local function epochs (reverse)

-- Parse epoch:
local lpeg = lpeg
local P, C, Cc, R = lpeg.P, lpeg.C, lpeg.Cc, lpeg.R
local arabic = C ((P'-' ^ -1 * R'09' ^ 1)--[[ / tonumber -- for some reason, inverts the sign]])
local function grammar (length, style, suffix, from1, BC)
	return Cc (length) * Cc (from1)
		 * (style and NAG (style) or arabic) * (suffix or '')
		 * (P' ' ^ 0 * P (BC) * Cc (true)) ^ -1 * -1
end	-- local function grammar (length, style, suffix, from1, BC)
	
local function epoch (str)
	local choice = P(false)
	for length, epoch in epochs (true) do
		choice = choice + grammar (length, epoch.style, epoch.suffix, epoch.from1, CalendarData.BC)
	end
	local length, from1, number, BC = choice:match (str)
	local year = (tonumber (number) * (from1 and length or 1) + (from1 and -1 or length > 1 and 1 or 0))
			   * (BC and -1 or 1)
	local start, finish = boundaries (year, length)
	return length, start, finish, CalendarData.epochs [length].API
end	-- local function epoch (str)
m.e = epoch

local function ribbon (start, finish, highlighted, period)
	local tabs = {}
	for current = start, finish, period do
		local epoch = year2epoch (current, period, true, true)
		local epoch_short = year2epoch (current, period, false, false)
		tabs [#tabs + 1] = '<div'
						.. (current == highlighted
								and ' class="current">[[' .. epoch
								or '>[[' .. epoch .. '|' .. epoch_short
							)
						.. ']]</div>'
	end
	return '\n<div class="row">' .. table.concat (tabs) .. '</div>'
end	-- local function ribbon (year, period, onesuffix)
m.r = ribbon

local function implement_navigator (length, start)
	local rows = ''
	local this = start
	for size, epoch in epochs (false) do
		if size >= length or size * 10 == length then
			local start, finish = boundaries (this, size * 10)
			rows = ribbon (start, finish, size >= length and this or 0, size) .. rows
			this = start
		end
	end
	return '<div class="tabbed">' .. rows .. '</div>'
end	-- local function implement_navigator (length, start)
m.i = implement_navigator

local function navigator (epoch_name)
	local length, start, finish = epoch (epoch_name)
	return implement_navigator (length, start)
end	-- local function navigator (epoch_name)
m.n = navigator

local function loadEpoch (epoch_name)
    if not epoch then
        return false, formatCalendarError (CalendarData.errorMissingEpoch)
    end
	local length, start, finish = epoch (epoch_name)
	if not length then
		return false, formatCalendarError (CalendarData.errorInvalidEpoch)
	end
    return true, length, start, finish
end	-- local function loadEpoch (epoch_name)

m [CalendarData.apiNavigator] = function (frame)
	local args = frame.args
	local ok, length, start, finish = loadEpoch (args [CalendarData.argEpoch])
	if not ok then
		return length
	end
	return implement_navigator (length, start)
end	-- m [CalendarData.apiNavigator] = function (frame)

return m