มอดูล:Easter

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

-- API
apiEaster            = "Calculate", -- public function name
argEasterYear        = 1,           -- index or name of the argument with year
argEasterMethod      = "method",    -- index or name of the argument with calculation method
argEasterOffset      = "day",       -- index or name of the argument with offset in days relative to the calculated Easter Sunday
argEasterFormat      = "format",    -- index or name of the argument with date output format (#time style)

-- errors
errorMissingYear     = "Missing mandatory argument 'year'",
errorInvalidYear     = "Incorrect argument 'year': '%s'",
errorInvalidOffset   = "Incorrect argument 'day': '%s'",
errorInvalidMethod   = "Incorrect argument 'method': '%s'",
errorYearOutOfRange  = "Easter dates are available between years 326 and 4099; year: %d",
errorIncorrectMethod = "Western or Orthodox Easter exists since 1583; year: %d",
errorUnknownMethod   = "Unknown method: %d",

methods = {
"Julian"      = 1, -- Eastern date in the Julian calendar
"Eastern"     = 2, -- Eastern date in the Gregorian calendar
"Orthodox"    = 2, -- alias for Eastern
"Coptic"      = 2, -- alias for Eastern
"Ethiopian"   = 2, -- alias for Eastern
"Western"     = 3, -- Western date in the Gregorian calendar
"Gregorian"   = 3, -- alias for Western
"Catholic"    = 3, -- alias for Western
"Roman"       = 3, -- alias for Western
"Revised"     = 4, -- defacto alias for Western for now
"Meletian"    = 4, -- alias for Revised
"Astro"       = 5, -- defacto alias for Western for now
},
-- other proposed or reformed algorithms are not supported (yet):
--
-- * 4 "Meletian" = "Revised": Revised Julian Calendar from 1923 used by some Orthodox churches 
--   with 900-year leap cycle, same as Gregorian until 2400 or so
-- * 5 "Astro": astronomically observed Nicean rule at the meridian of Jerusalem (Aleppo 1997 proposal), 
--   differs from Gregorian sometimes
-- * 6 based on (equivalently) a range of valid dates in April:
--   * 61 "First": 1st Sunday in April = Sunday in 1–7 April
--   * 67 "Pepuzite": Sunday after 6 April = Sunday in 7–13 April
--   * 68 "April" = "Second": 2nd Sunday in April = Sunday in 8–14 April
--   * 69 "Fixed" = "UK": day after second Saturday in April = Sunday in 9–15 April
-- * 7 based on (equivalently) a range of valid days of the year (DOY):
--   * 75 "W14": Sunday of ISO week 14 = Sunday in 095–101
--   * 79 "Fifteen": 15th Sunday of the year: Sunday in 099–105
--   * 72 "W15": Sunday of ISO week 15 = Sunday in 102–108
-- * "Symmetry": Sym454/Sym010: Sunday of week 14 in a 293-year leap cycle
-- 
-- Breaking from the Biblical week cycle, any day of the week in the Gregorian calendar:
-- 
-- * "World": day 099, Sunday in the World Calendar
-- * "Positivist": day 098, Sunday in the Positivst Calendar
-- * "Quartodecimanism": Nisan 14 in the contemporary Jewish/Hebrew calendar, pre-Nicean
-- * "Quintodecimanism": Nisan 15 in the contemporary Jewish/Hebrew calendar, pre-Nicean

relativeDates = {
"Septuagesima"             = -63,
"Sexagesima"               = -56,
"Fat Thursday"             = -52,
"Quinquagesima"            = -49, -- Estomihi, Shrove Sunday
"Shrove Monday"            = -48, -- Rose Monday
"Shrove Tuesday"           = -47, -- Mardi Gras, Carnival
"Ash Wednesday"            = -46,
"Invocabit Sunday"         = -42,
"Reminiscere Sunday"       = -35,
"Oculi Sunday"             = -28,
"Laetare Sunday"           = -21, -- Mothering Sunday
"Holy Week"                =  -7,
"Palm Sunday"              =  -7,
"Holy Monday"              =  -6, 
"Holy Tuesday"             =  -5, 
"Holy Wednesday"           =  -4, 
"Maundy Thursday"          =  -3,
"Good Friday"              =  -2, -- Crucifixion
"Holy Saturday"            =  -1, 
"Easter"                   =   0, -- Easter Sunday, Resurrection
"Easter Monday"            =   1,
"Divine Mercy"             =   7,
"Misericordias Domini"     =  14,
"Jubilate Sunday"          =  21,
"Cantate Sunday"           =  28,
"Vocem jucunditatis"       =  35,
"Ascension Thursday"       =  39, -- Ascension
"Pentecost"                =  49, -- Whitsun
"Whit Monday"              =  50,
"Trinity Sunday"           =  56, 
"Corpus Christi"           =  60, -- Body and Blood of Christ
"Sacred Heart"             =  68,
"Immaculate Heart"         =  69,
},
}

local function formatEasterError(message, ...)
if select('#', ... ) > 0 then
message = string.format(message, ...)
end
return "" .. message .. ""
end

local function loadEasterYear(year)
if not year then
return false, formatEasterError(EasterData.errorMissingYear)
end
local result = tonumber(year)
if not result or math.floor(result) ~= result then
return false, formatEasterError(EasterData.errorInvalidYear, year)
end

return true, result
end

local function loadEasterMethod(method, year)
local result = EasterData.defaultMethod
if method then
result = EasterData.methodsmethod
if not result then
return false, formatEasterError(EasterData.errorInvalidMethod, method)
end
end

if year  EasterData.maximumOffset then
return false, formatEasterError(EasterData.errorInvalidOffset, day)
end

if offset  1 then
return true, string.format(" +%d days", offset)
end
end

local function loadEasterFormat(fmt)
if fmt == EasterData.noFormat then
return true, nil
elseif not fmt then
return true, EasterData.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
4 = the revised calculation based on the
Meletian 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  4099 then
-- Easter dates are valid for years between 326 and 4099
-- Method 2 would have to support dates in June thereafter
return 0, formatEasterError(EasterData.errorYearOutOfRange, year)
end
if year  2400) and method ~= 4 then
-- The Revised Julian Calendar is not really supported yet
return 0, formatEasterError(EasterData.errorYearOutOfRange, 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

-- Julian:
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
-- Eastern/Orthodox:
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
-- Gregorian:
elseif method == 3 or method == 4 then
-- calculate paschal full moon (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, formatEasterError(EasterData.errorUnknownMethod, method)
end
-- when the original calculation is converted to the Gregorian calendar,
-- Easter Sunday can occur in May or even in June in the distant future
if d > 92 then
return 6, d - 92  -- June
elseif d > 61 then
return 5, d - 61  -- May
elseif d > 31 then
return 4, d - 31  -- April
else
return 3, d       -- March
end
end

local function Easter(args)
local ok
local year
ok, year = loadEasterYear(argsEasterData.argEasterYear)
if not ok then
return year
end

local method
ok, method = loadEasterMethod(argsEasterData.argEasterMethod, year)
if not ok then
return method
end

local offset
ok, offset = loadEasterOffset(argsEasterData.argEasterOffset)
if not ok then
return offset
end

local format
ok, format = loadEasterFormat(argsEasterData.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

mEasterData.apiEaster = function (frame)
return Easter(frame.args)
end

return m