Module: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 Sunday"] = 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.methods[method]

if not result then

return false, formatEasterError(EasterData.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 = EasterData.relativeDates

local offset = tonumber(day)

if not offset then

offset = data[day]

end

if not offset or offset ~= math.floor(offset) or offset < EasterData.minimumOffset or offset > EasterData.maximumOffset then

return false, formatEasterError(EasterData.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 == 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 < 326 or 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 < 1583 and method ~= 1 then

-- Western or Orthodox Easter is valid since 1583

return 0, formatEasterError(EasterData.errorIncorrectMethod, year)

end

if (year < 1600 or 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(args[EasterData.argEasterYear])

if not ok then

return year

end

local method

ok, method = loadEasterMethod(args[EasterData.argEasterMethod], year)

if not ok then

return method

end

local offset

ok, offset = loadEasterOffset(args[EasterData.argEasterOffset])

if not ok then

return offset

end

local format

ok, format = loadEasterFormat(args[EasterData.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[EasterData.apiEaster] = function (frame)

return Easter(frame.args)

end

return m