Module:Easter
local m = {}
 
 local EasterData = {
     defaultMethod = 3,        -- default method of Easter date calculation when Easter type is not given
     defaultFormat = &quot;Y-m-d&quot;,  -- default date output format
     noFormat      = &quot;none&quot;,   -- prevent from final date formatting
     defaultOffset = 0,        -- the Easter date
     minimumOffset = -63,      -- Septuagesima
     maximumOffset = 68,       -- Feast of the Sacred Heart
  
     -- API
     apiEaster            = &quot;Calculate&quot;, -- public function name
     argEasterYear        = 1,           -- index or name of the argument with year
     argEasterMethod      = &quot;method&quot;,    -- index or name of the argument with calculation method
     argEasterOffset      = &quot;day&quot;,       -- index or name of the argument with offset in days relative to the calculated Easter Sunday
     argEasterFormat      = &quot;format&quot;,    -- index or name of the argument with date output format (#time style)
  
     -- errors
     errorMissingYear     = &quot;Missing mandatory argument with year&quot;,
     errorInvalidYear     = &quot;The argument with year value is incorrect: '%s'&quot;,
     errorInvalidOffset   = &quot;Incorrect argument 'day': '%s'&quot;,
     errorInvalidMethod   = &quot;Incorrect argument 'method': '%s'&quot;,
     errorYearOutOfRange  = &quot;Easter dates are available between years 326 and 4099; year: %d&quot;,
     errorIncorrectMethod = &quot;Western or Orthodox Easter exists since 1583; year: %d&quot;,
     errorUnknownMethod   = &quot;Unknown method: %d&quot;,
  
     methods = {
         [&quot;Julian&quot;]      = 1,
         [&quot;Eastern&quot;]     = 2,
         [&quot;Orthodox&quot;]    = 2, -- alias for Eastern
         [&quot;Western&quot;]     = 3,
         [&quot;Roman&quot;]       = 3, -- alias for Roman
     },
  
     relativeDates = {
         [&quot;Septuagesima&quot;]             = -63,
         [&quot;Sexagesima&quot;]               = -56,
         [&quot;Fat Thursday&quot;]             = -52,
         [&quot;Quinquagesima&quot;]            = -49,
         [&quot;Ash Wednesday&quot;]            = -46,
         [&quot;Palm Sunday&quot;]              =  -7,
         [&quot;Maundy Thursday&quot;]          =  -3,
         [&quot;Good Friday&quot;]              =  -2,
         [&quot;Holy Saturday&quot;]            =  -1, 
         [&quot;Easter&quot;]                   =   0,
         [&quot;Easter Monday&quot;]            =   1,
         [&quot;Ascension Thursday&quot;]       =  39,
         [&quot;Pentecost&quot;]                =  49,
         [&quot;Corpus Christi&quot;]           =  60,
     },
 }
  
 local function formatEasterError(message, ...)
     if select('#', ... ) &gt; 0 then
         message = string.format(message, ...)
     end
     return &quot;&lt;span class=\&quot;error\&quot;&gt;&quot; .. message .. &quot;&lt;/span&gt;&quot;
 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 &lt; 1583 then
         result = 1
     end
  
     return true, result
 end
  
 local function loadEasterOffset(day)
     if not day then
         return true, &quot;&quot;
     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 &lt; EasterData.minimumOffset or offset &gt; EasterData.maximumOffset then
         return false, formatEasterError(EasterData.errorInvalidOffset, day)
     end
  
     if offset &lt; -1 then
         return true, string.format(&quot; %d days&quot;, offset)
     elseif offset == -1 then
         return true, &quot; -1 day&quot;
     elseif offset == 0 then
         return true, &quot;&quot;
     elseif offset == 1 then
         return true, &quot; +1 day&quot;
     else -- if offset &gt; 1 then
         return true, string.format(&quot; +%d days&quot;, 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
  
  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 &lt; 326 or year &gt; 4099 then
         -- Easter dates are valid for years between 326 and 4099
         return 0, formatEasterError(EasterData.errorYearOutOfRange, year)
     end
     if year &lt; 1583 and method ~= 1 then
         -- Western or Orthodox Easter is valid since 1583
         return 0, formatEasterError(EasterData.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 &gt; 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 &gt; 26 then
             temp = temp - 1
         end
         if firstDig &gt; 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 &gt; 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 &gt; 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(EasterData.errorUnknownMethod, method)
     end
     if d &gt; 61 then
         -- when the oryginal calculation os converted to the Gregorian
         -- calendar, Easter Sunday can occur in May
         return 5, d - 61
     elseif d &gt; 31 then
         return 4, d - 31
     else
         return 3, d
     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(&quot;%04d-%02d-%02d%s&quot;, 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