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 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 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