Function to return date of Easter for the given year
Asked Answered
C

10

38

So, here's a funny little programming challenge. I was writing a quick method to determine all the market holidays for a particular year, and then I started reading about Easter and discovered just how crazy* the logic is for determining its date--the first Sunday after the Paschal Full Moon following the spring equinox! Does anybody know of an existing function to calculate the date of Easter for a given year?

Granted, it's probably not all that hard to do; I just figured I'd ask in case somebody's already done this. (And that seems very likely.)

UPDATE: Actually, I'm really looking for the date of Good Friday (the Friday before Easter)... I just figured Easter would get me there. And since I'm in the U.S., I assume I'm looking for the Catholic Easter? But perhaps someone can correct me on that if I'm wrong.

*By "crazy" I meant, like, involved. Not anything offensive...

Cassatt answered 3/2, 2010 at 14:19 Comment(7)
Which Easter? Catholic, Orthodox, or Coptic?Plossl
What is the first year for which it should work correctly?Samal
Another nice write-up about "calculating easter": simple-talk.com/community/blogs/philfactor/archive/2009/01/18/… . And in T-SQL, of all things.Veronicaveronika
Excellent question. I'm actually embarrassed I didn't specify this at the outset; in reality, though, I'm actually not interested in Easter so much as Good Friday, whose date just happens to depend on Easter. I'll update the question.Cassatt
@AJ: Ha, good question. Certainly some year AD... ;)Cassatt
“There are many indications that the sole important application of arithmetic in Europe during the Middle Ages was the calculation of Easter's date.” – KnuthBulgaria
@JohnSaunders dateutil supports julian, orthodox and western.Lento
M
17

in SQL Server Easter Sunday would look like this, scroll down for Good Friday

CREATE FUNCTION dbo.GetEasterSunday 
( @Y INT ) 
RETURNS SMALLDATETIME 
AS 
BEGIN 
    DECLARE     @EpactCalc INT,  
        @PaschalDaysCalc INT, 
        @NumOfDaysToSunday INT, 
        @EasterMonth INT, 
        @EasterDay INT 

    SET @EpactCalc = (24 + 19 * (@Y % 19)) % 30 
    SET @PaschalDaysCalc = @EpactCalc - (@EpactCalc / 28) 
    SET @NumOfDaysToSunday = @PaschalDaysCalc - ( 
        (@Y + @Y / 4 + @PaschalDaysCalc - 13) % 7 
    ) 

    SET @EasterMonth = 3 + (@NumOfDaysToSunday + 40) / 44 

    SET @EasterDay = @NumOfDaysToSunday + 28 - ( 
        31 * (@EasterMonth / 4) 
    ) 

    RETURN 
    ( 
        SELECT CONVERT 
        (  SMALLDATETIME, 
                 RTRIM(@Y)  
            + RIGHT('0'+RTRIM(@EasterMonth), 2)  
            + RIGHT('0'+RTRIM(@EasterDay), 2)  
        ) 
    ) 

END 
GO

Good Friday is like this and it uses the Easter function above

CREATE FUNCTION dbo.GetGoodFriday 
( 
    @Y INT 
) 
RETURNS SMALLDATETIME 
AS 
BEGIN 
    RETURN (SELECT dbo.GetEasterSunday(@Y) - 2) 
END 
GO

From here: http://web.archive.org/web/20070611150639/http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-calendar-table.html

Medardas answered 3/2, 2010 at 14:26 Comment(2)
This only works for years 1900 - 2199. Before 1900 and after 2199, it deviates from NOAA's Easter calculation script. The benefit of this script is that it's short and sweet. Here's NOAA's script Thanks for posting this.Electrophilic
@Electrophilic The deviation between the simplified version in this answer and NOAA version indeed starts as of 1899 (April 3 1899 (simple) vs Apr 2 1899 (NOAA)). But they differ as soon as 2100 (Mar 27 2100 (Simple) vs March 28 2100 (NOAA)) I'll post an answer with the NOAA algorithm sometime later today.Fugal
K
42

Python: using dateutil's easter() function.

>>> from dateutil.easter import *
>>> print easter(2010)
2010-04-04
>>> print easter(2011)
2011-04-24

The functions gets, as an argument, the type of calculation you like:

EASTER_JULIAN   = 1
EASTER_ORTHODOX = 2
EASTER_WESTERN  = 3

You can pick the one relevant to the US.

Reducing two days from the result would give you Good Friday:

>>> from datetime import timedelta
>>> d = timedelta(days=-2)
>>> easter(2011)
datetime.date(2011, 4, 24)
>>> easter(2011)+d
datetime.date(2011, 4, 22)

Oddly enough, someone was iterating this, and published the results in Wikipedia's article about the algorithm:

alt text

Klink answered 3/2, 2010 at 14:23 Comment(0)
M
17

in SQL Server Easter Sunday would look like this, scroll down for Good Friday

CREATE FUNCTION dbo.GetEasterSunday 
( @Y INT ) 
RETURNS SMALLDATETIME 
AS 
BEGIN 
    DECLARE     @EpactCalc INT,  
        @PaschalDaysCalc INT, 
        @NumOfDaysToSunday INT, 
        @EasterMonth INT, 
        @EasterDay INT 

    SET @EpactCalc = (24 + 19 * (@Y % 19)) % 30 
    SET @PaschalDaysCalc = @EpactCalc - (@EpactCalc / 28) 
    SET @NumOfDaysToSunday = @PaschalDaysCalc - ( 
        (@Y + @Y / 4 + @PaschalDaysCalc - 13) % 7 
    ) 

    SET @EasterMonth = 3 + (@NumOfDaysToSunday + 40) / 44 

    SET @EasterDay = @NumOfDaysToSunday + 28 - ( 
        31 * (@EasterMonth / 4) 
    ) 

    RETURN 
    ( 
        SELECT CONVERT 
        (  SMALLDATETIME, 
                 RTRIM(@Y)  
            + RIGHT('0'+RTRIM(@EasterMonth), 2)  
            + RIGHT('0'+RTRIM(@EasterDay), 2)  
        ) 
    ) 

END 
GO

Good Friday is like this and it uses the Easter function above

CREATE FUNCTION dbo.GetGoodFriday 
( 
    @Y INT 
) 
RETURNS SMALLDATETIME 
AS 
BEGIN 
    RETURN (SELECT dbo.GetEasterSunday(@Y) - 2) 
END 
GO

From here: http://web.archive.org/web/20070611150639/http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-calendar-table.html

Medardas answered 3/2, 2010 at 14:26 Comment(2)
This only works for years 1900 - 2199. Before 1900 and after 2199, it deviates from NOAA's Easter calculation script. The benefit of this script is that it's short and sweet. Here's NOAA's script Thanks for posting this.Electrophilic
@Electrophilic The deviation between the simplified version in this answer and NOAA version indeed starts as of 1899 (April 3 1899 (simple) vs Apr 2 1899 (NOAA)). But they differ as soon as 2100 (Mar 27 2100 (Simple) vs March 28 2100 (NOAA)) I'll post an answer with the NOAA algorithm sometime later today.Fugal
B
5

When it came for me to write this (traffic prediction based on day of week and holiday), I gave up on trying to write it by myself. I found it somewhere on the net. The code was public domain, but...

sigh

see for yourself.

void dateOfEaster(struct tm* p)
{
    int Y = p->tm_year;
    int a = Y % 19;
    int b = Y / 100;
    int c = Y % 100;
    int d = b / 4;
    int e = b % 4;
    int f = (b + 8) / 25;
    int g = (b - f + 1) / 3;
    int h = (19 * a + b - d - g + 15) % 30;
    int i = c / 4;
    int k = c % 4;
    int L = (32 + 2 * e + 2 * i - h - k) % 7;
    int m = (a + 11 * h + 22 * L) / 451;
    p->tm_mon = ((h + L - 7 * m + 114) / 31 ) - 1;
    p->tm_mday = ((h + L - 7 * m + 114) % 31) + 1;
    p->tm_hour = 12;
    const time_t tmp = mktime(p);
    *p = *localtime(&tmp);  //recover yday from mon+mday
}

Some questions are better left unasked.

I feel lucky that all moving holidays in my country are a fixed offset from the date of Easter.

Barri answered 3/2, 2010 at 14:41 Comment(1)
I think this is actually Donald Knuth's algorithm: linuxtopia.org/online_books/programming_books/…Eckstein
F
3

The SQL Server function below is more general than the accepted answer

The accepted answer is only correct for the range (inclusive) : 1900-04-15 to 2099-04-12

It uses the algorithm provided by The United States Naval Observatory (USNO)

http://aa.usno.navy.mil/faq/docs/easter.php

CREATE FUNCTION dbo.GetEasterSunday (@Y INT)
RETURNS DATETIME
AS
    BEGIN 

        -- Source of algorithm : http://aa.usno.navy.mil/faq/docs/easter.php

        DECLARE @c INT = @Y / 100
        DECLARE @n INT = @Y - 19 * (@Y / 19)
        DECLARE @k INT = (@c - 17) / 25
        DECLARE @i INT = @c - @c / 4 - (@c - @k) / 3 + 19 * @n + 15
        SET @i = @i - 30 * (@i / 30)
        SET @i = @i - (@i / 28) * (1 - (@i / 28) * (29 / (@i + 1)) * ((21 - @n) / 11))
        DECLARE @j INT = @Y + @Y / 4 + @i + 2 - @c + @c / 4
        SET @j = @j - 7 * (@j / 7)
        DECLARE @l INT = @i - @j
        DECLARE @m INT = 3 + (@l + 40) / 44
        DECLARE @d INT = @l + 28 - 31 * (@m / 4)

        RETURN 
    ( 
        SELECT CONVERT 
        (  DATETIME, 
                 RTRIM(@Y)  
            + RIGHT('0'+RTRIM(@m), 2)  
            + RIGHT('0'+RTRIM(@d), 2)  
    ) 
    )
    END 


GO
Fugal answered 1/8, 2016 at 12:13 Comment(0)
W
2

VB .NET Functions for Greek Orthodox and Catholic Easter:

Public Shared Function OrthodoxEaster(ByVal Year As Integer) As Date
    Dim a = Year Mod 19
    Dim b = Year Mod 7
    Dim c = Year Mod 4
    Dim d = (19 * a + 16) Mod 30
    Dim e = (2 * c + 4 * b + 6 * d) Mod 7
    Dim f = (19 * a + 16) Mod 30
    Dim key = f + e + 3
    Dim month = If((key > 30), 5, 4)
    Dim day = If((key > 30), key - 30, key)
    Return New DateTime(Year, month, day)
End Function

Public Shared Function CatholicEaster(ByVal Year As Integer) As DateTime
    Dim month = 3
    Dim a = Year Mod 19 + 1
    Dim b = Year / 100 + 1
    Dim c = (3 * b) / 4 - 12
    Dim d = (8 * b + 5) / 25 - 5
    Dim e = (5 * Year) / 4 - c - 10
    Dim f = (11 * a + 20 + d - c) Mod 30
    If f = 24 Then f += 1
    If (f = 25) AndAlso (a > 11) Then f += 1
    Dim g = 44 - f
    If g < 21 Then g = g + 30
    Dim day = (g + 7) - ((e + g) Mod 7)
    If day > 31 Then
        day = day - 31
        month = 4
    End If
    Return New DateTime(Year, month, day)
End Function
Winthorpe answered 28/12, 2018 at 13:4 Comment(0)
D
1

The below code determines Easter through powershell:

function Get-DateOfEaster {
    param(
        [Parameter(ValueFromPipeline)]
        $theYear=(Get-Date).Year
        )

    if($theYear -lt 1583) {
        return $null
    } else {

        # Step 1: Divide the theYear by 19 and store the
        # remainder in variable A.  Example: If the theYear
        # is 2000, then A is initialized to 5.

        $a = $theYear % 19

        # Step 2: Divide the theYear by 100.  Store the integer
        # result in B and the remainder in C.

        $c = $theYear % 100
        $b = ($theYear -$c) / 100

        # Step 3: Divide B (calculated above).  Store the
        # integer result in D and the remainder in E.

        $e = $b % 4
        $d = ($b - $e) / 4

        # Step 4: Divide (b+8)/25 and store the integer
        # portion of the result in F.

        $f = [math]::floor(($b + 8) / 25)

        # Step 5: Divide (b-f+1)/3 and store the integer
        # portion of the result in G.

        $g = [math]::floor(($b - $f + 1) / 3)

        # Step 6: Divide (19a+b-d-g+15)/30 and store the
        # remainder of the result in H.

        $h = (19 * $a + $b - $d - $g + 15) % 30

        # Step 7: Divide C by 4.  Store the integer result
        # in I and the remainder in K.

        $k = $c % 4
        $i = ($c - $k) / 4

        # Step 8: Divide (32+2e+2i-h-k) by 7.  Store the
        # remainder of the result in L.

        $l = (32 + 2 * $e + 2 * $i - $h - $k) % 7

        # Step 9: Divide (a + 11h + 22l) by 451 and
        # store the integer portion of the result in M.

        $m = [math]::floor(($a + 11 * $h + 22 * $l) / 451)

        # Step 10: Divide (h + l - 7m + 114) by 31.  Store
        # the integer portion of the result in N and the
        # remainder in P.

        $p = ($h + $l - 7 * $m + 114) % 31
        $n = (($h + $l - 7 * $m + 114) - $p) / 31

        # At this point p+1 is the day on which Easter falls.
        # n is 3 for March and 4 for April.

        $DateTime = New-Object DateTime $theyear, $n, ($p+1), 0, 0, 0, ([DateTimeKind]::Utc)
        return $DateTime
    }
}

$eastersunday=Get-DateOfEaster 2015
Write-Host $eastersunday
Damper answered 29/3, 2018 at 14:9 Comment(0)
D
0

Found this Excel formula somewhere
Assuming cell A1 contains year e.g. 2020

ROUND(DATE(A1;4;1)/7+MOD(19*MOD(A1;19)-7;30)*0,14;0)*7-6

Converted to T-SQL lead me to this:

DECLARE @yr INT=2020
SELECT DATEADD(dd, ROUND(DATEDIFF(dd, '1899-12-30', DATEFROMPARTS(@yr, 4, 1)) / 7.0 + ((19.0 * (@yr % 19) - 7) % 30) * 0.14, 0) * 7.0 - 6, -2)
Desmond answered 31/12, 2019 at 14:41 Comment(0)
P
0

In JS, taken from here.

var epoch=2444238.5,elonge=278.83354,elongp=282.596403,eccent=.016718,sunsmax=149598500,sunangsiz=.533128,mmlong=64.975464,mmlongp=349.383063,mlnode=151.950429,minc=5.145396,mecc=.0549,mangsiz=.5181,msmax=384401,mparallax=.9507,synmonth=29.53058868,lunatbase=2423436,earthrad=6378.16,PI=3.141592653589793,epsilon=1e-6;function sgn(x){return x<0?-1:x>0?1:0}function abs(x){return x<0?-x:x}function fixAngle(a){return a-360*Math.floor(a/360)}function toRad(d){return d*(PI/180)}function toDeg(d){return d*(180/PI)}function dsin(x){return Math.sin(toRad(x))}function dcos(x){return Math.cos(toRad(x))}function toJulianTime(date){var year,month,day;year=date.getFullYear();var m=(month=date.getMonth()+1)>2?month:month+12,y=month>2?year:year-1,d=(day=date.getDate())+date.getHours()/24+date.getMinutes()/1440+(date.getSeconds()+date.getMilliseconds()/1e3)/86400,b=isJulianDate(year,month,day)?0:2-y/100+y/100/4;return Math.floor(365.25*(y+4716)+Math.floor(30.6001*(m+1))+d+b-1524.5)}function isJulianDate(year,month,day){if(year<1582)return!0;if(year>1582)return!1;if(month<10)return!0;if(month>10)return!1;if(day<5)return!0;if(day>14)return!1;throw"Any date in the range 10/5/1582 to 10/14/1582 is invalid!"}function jyear(td,yy,mm,dd){var z,f,alpha,b,c,d,e;return f=(td+=.5)-(z=Math.floor(td)),b=(z<2299161?z:z+1+(alpha=Math.floor((z-1867216.25)/36524.25))-Math.floor(alpha/4))+1524,c=Math.floor((b-122.1)/365.25),d=Math.floor(365.25*c),e=Math.floor((b-d)/30.6001),{day:Math.floor(b-d-Math.floor(30.6001*e)+f),month:Math.floor(e<14?e-1:e-13),year:Math.floor(mm>2?c-4716:c-4715)}}function jhms(j){var ij;return j+=.5,ij=Math.floor(86400*(j-Math.floor(j))+.5),{hour:Math.floor(ij/3600),minute:Math.floor(ij/60%60),second:Math.floor(ij%60)}}function jwday(j){return Math.floor(j+1.5)%7}function meanphase(sdate,k){var t,t2;return 2415020.75933+synmonth*k+1178e-7*(t2=(t=(sdate-2415020)/36525)*t)-155e-9*(t2*t)+33e-5*dsin(166.56+132.87*t-.009173*t2)}function truephase(k,phase){var t,t2,t3,pt,m,mprime,f,apcor=!1;if(pt=2415020.75933+synmonth*(k+=phase)+1178e-7*(t2=(t=k/1236.85)*t)-155e-9*(t3=t2*t)+33e-5*dsin(166.56+132.87*t-.009173*t2),m=359.2242+29.10535608*k-333e-7*t2-347e-8*t3,mprime=306.0253+385.81691806*k+.0107306*t2+1236e-8*t3,f=21.2964+390.67050646*k-.0016528*t2-239e-8*t3,phase<.01||abs(phase-.5)<.01?(pt+=(.1734-393e-6*t)*dsin(m)+.0021*dsin(2*m)-.4068*dsin(mprime)+.0161*dsin(2*mprime)-4e-4*dsin(3*mprime)+.0104*dsin(2*f)-.0051*dsin(m+mprime)-.0074*dsin(m-mprime)+4e-4*dsin(2*f+m)-4e-4*dsin(2*f-m)-6e-4*dsin(2*f+mprime)+.001*dsin(2*f-mprime)+5e-4*dsin(m+2*mprime),apcor=!0):(abs(phase-.25)<.01||abs(phase-.75)<.01)&&(pt+=(.1721-4e-4*t)*dsin(m)+.0021*dsin(2*m)-.628*dsin(mprime)+.0089*dsin(2*mprime)-4e-4*dsin(3*mprime)+.0079*dsin(2*f)-.0119*dsin(m+mprime)-.0047*dsin(m-mprime)+3e-4*dsin(2*f+m)-4e-4*dsin(2*f-m)-6e-4*dsin(2*f+mprime)+.0021*dsin(2*f-mprime)+3e-4*dsin(m+2*mprime)+4e-4*dsin(m-2*mprime)-3e-4*dsin(2*m+mprime),pt+=phase<.5?.0028-4e-4*dcos(m)+3e-4*dcos(mprime):4e-4*dcos(m)-.0028-3e-4*dcos(mprime),apcor=!0),!apcor)throw"Error calculating moon phase!";return pt}function phasehunt(sdate,phases){var adate,k1,k2,nt1,nt2,yy,mm,dd,jyearResult=jyear(adate=sdate-45,yy,mm,dd);for(yy=jyearResult.year,mm=jyearResult.month,dd=jyearResult.day,adate=nt1=meanphase(adate,k1=Math.floor(12.3685*(yy+1/12*(mm-1)-1900)));nt2=meanphase(adate+=synmonth,k2=k1+1),!(nt1<=sdate&&nt2>sdate);)nt1=nt2,k1=k2;return phases[0]=truephase(k1,0),phases[1]=truephase(k1,.25),phases[2]=truephase(k1,.5),phases[3]=truephase(k1,.75),phases[4]=truephase(k2,0),phases}function kepler(m,ecc){var e,delta;e=m=toRad(m);do{e-=(delta=e-ecc*Math.sin(e)-m)/(1-ecc*Math.cos(e))}while(abs(delta)>epsilon);return e}function getMoonPhase(julianDate){var Day,N,M,Ec,Lambdasun,ml,MM,MN,Ev,Ae,MmP,mEc,lP,lPP,NP,y,x,MoonAge,MoonPhase,MoonDist,MoonDFrac,MoonAng,F,SunDist,SunAng;return N=fixAngle(360/365.2422*(Day=julianDate-epoch)),Ec=kepler(M=fixAngle(N+elonge-elongp),eccent),Ec=Math.sqrt((1+eccent)/(1-eccent))*Math.tan(Ec/2),Lambdasun=fixAngle((Ec=2*toDeg(Math.atan(Ec)))+elongp),F=(1+eccent*Math.cos(toRad(Ec)))/(1-eccent*eccent),SunDist=sunsmax/F,SunAng=F*sunangsiz,ml=fixAngle(13.1763966*Day+mmlong),MM=fixAngle(ml-.1114041*Day-mmlongp),MN=fixAngle(mlnode-.0529539*Day),MmP=MM+(Ev=1.2739*Math.sin(toRad(2*(ml-Lambdasun)-MM)))-(Ae=.1858*Math.sin(toRad(M)))-.37*Math.sin(toRad(M)),lPP=(lP=ml+Ev+(mEc=6.2886*Math.sin(toRad(MmP)))-Ae+.214*Math.sin(toRad(2*MmP)))+.6583*Math.sin(toRad(2*(lP-Lambdasun))),NP=MN-.16*Math.sin(toRad(M)),y=Math.sin(toRad(lPP-NP))*Math.cos(toRad(minc)),x=Math.cos(toRad(lPP-NP)),toDeg(Math.atan2(y,x)),NP,toDeg(Math.asin(Math.sin(toRad(lPP-NP))*Math.sin(toRad(minc)))),MoonAge=lPP-Lambdasun,MoonPhase=(1-Math.cos(toRad(MoonAge)))/2,MoonDist=msmax*(1-mecc*mecc)/(1+mecc*Math.cos(toRad(MmP+mEc))),MoonAng=mangsiz/(MoonDFrac=MoonDist/msmax),mparallax/MoonDFrac,{moonIllumination:MoonPhase,moonAgeInDays:synmonth*(fixAngle(MoonAge)/360),distanceInKm:MoonDist,angularDiameterInDeg:MoonAng,distanceToSun:SunDist,sunAngularDiameter:SunAng,moonPhase:fixAngle(MoonAge)/360}}function getMoonInfo(date){return null==date?{moonPhase:0,moonIllumination:0,moonAgeInDays:0,distanceInKm:0,angularDiameterInDeg:0,distanceToSun:0,sunAngularDiameter:0}:getMoonPhase(toJulianTime(date))}function getEaster(year){var previousMoonInfo,moonInfo,fullMoon=new Date(year,2,21),gettingDarker=void 0;do{previousMoonInfo=getMoonInfo(fullMoon),fullMoon.setDate(fullMoon.getDate()+1),moonInfo=getMoonInfo(fullMoon),void 0===gettingDarker?gettingDarker=moonInfo.moonIllumination<previousMoonInfo.moonIllumination:gettingDarker&&moonInfo.moonIllumination>previousMoonInfo.moonIllumination&&(gettingDarker=!1)}while(gettingDarker&&moonInfo.moonIllumination<previousMoonInfo.moonIllumination||!gettingDarker&&moonInfo.moonIllumination>previousMoonInfo.moonIllumination);for(fullMoon.setDate(fullMoon.getDate()-1);0!==fullMoon.getDay();)fullMoon.setDate(fullMoon.getDate()+1);return fullMoon}

Then run getEaster(2020); // -> Sun Apr 12 2020

Pika answered 26/9, 2020 at 5:26 Comment(0)
I
0

Oracle, gives correct Easter dates between 1900 and 2078:

to_date('18991230','yyyymmdd') + round((to_date(callendar_year||'04','yyyymm') -
to_date('18991230','yyyymmdd')) / 7 + mod(19 * MOD(callendar_year, 19) + 23, 30) * 0.14, 0) * 7-6
Ignorant answered 12/4, 2023 at 11:22 Comment(0)
F
0

Although your question asks specifically for Catholic (Gregorian calendar) Easter, I'm going to give three historically-used algorithms, because that makes it easier to explain the math.

Note that the Parameter to Python's datetime.date.fromordinal function is the Rata Die day number, with day 1 = 0001-01-01.

Hebrew Calendar

This is the Sunday that falls during Passover.

import datetime

def hebrew_easter(year):
    lunation = (235 * year + 6) // 19 - 9
    week, molad = divmod(765433 * lunation + 65451, 181440)
    if (7 * year + 6) % 19 < 12 and (molad >= 172000):
        week += 1
    return datetime.date.fromordinal(week * 7)

Gregorian-calendar dates range from March 27 to April 30 (for years 1900-2213).

lunation represents the number of lunar months elapsed since the Rata Die epoch. The Hebrew Calendar uses the Metonic Cycle of 19 years = 235 synodic months. The lunar phase cycle is assumed to be a constant 765433/25920 (29.53059413580247) days.

The product of months per year (235/19) and days per month (765433/25920) results in an average year length of 35975351/98496 (365.24682220597794) days. This is slightly longer than the Gregorian calendar's average year of 365.2425 days. So on average, the date of Jewish holidays drifts 1 day later every 231 years.

molad, the remainder of the division, is related to the timing of the relevant lunar conjunction within the week.

The if statement is there to deal with some special rules of the Hebrew calendar to ensure that:

  • Rosh Hashana never falls on Sunday, Wednesday, or Friday (because this would cause a couple of the high holy days to fall on inconvenient days of the week).
  • The number of days in a year must be either 353, 354, 355, 383, 384, 385.

One of these rules, named deḥiyyat GaTaRaD, causes Rosh Hashana to be postponed from Tuesday to Thursday, which results in the preceding Passover being postponed from Sunday to Tuesday. This requires Easter to be postponed one week.

Note that the constant 172000 is somewhat arbitrarily selected. To make the above algorithm work for the years 0001 to 9999 (the range supported by Python's datetime class), any value between 171999 and 172071 will work. I chose 172000 simply for being the “roundest” number in that range. If for some reason, you need to work with a wider range of years, this constant may need to be adjusted.

Julian Calendar

The Council of Nicea in 325 CE decided not to use the actual Jewish calendar to set the date of Easter, but to perform their own calculation.

import datetime 

def julian_easter(year):
    leap_months = (7 * year + 8) // 19 - 9
    week = ((6725 * year + 18) // 19 + 30 * leap_months + year // 4 + 5) // 7
    return datetime.date.fromordinal(week * 7)

Gregorian-calendar dates range from April 5 to May 8 (for years 2011-2172).

This calculation uses the same 235/19 Metonic cycle as the Hebrew calendar, but with a couple of added quirks:

  • The February 29 leap day is essentially ignored in the calculation, and only added at the end.
  • Lunar leap months are treated as 30 days, while the 12 "regular" lunar months are 6725/228 (29.49561403508772) days.

Otherwise, the calculation is pretty straightforward.

Gregorian Calendar

import datetime

def gregorian_easter(year):
    century = year // 100
    lunar_adj = (8 * century + 13) // 25
    solar_adj = -century + century // 4
    total_adj = solar_adj + lunar_adj
    leap_months = (210 * year - year % 19 + 19 * total_adj + 266) // 570
    full_moon = (6725 * year + 18) // 19 + 30 * leap_months - lunar_adj + year // 4 + 3
    if 286 <= (total_adj + year % 19 * 11) % 30 * 19 - year % 19 <= 312:
        full_moon -= 1
    week = full_moon // 7 - 38
    return datetime.date.fromordinal(week * 7)

Dates range from March 22 to April 25.

Recall from the “Hebrew Calendar” section that a Metonic year (235/19 synodic months) is 365.2468 days (rounding to a reasonable number of digits). This is shorter than the Julian calendar's average year of 365.25 days. So we need to adjust the year length by 0.0032 day per year, or 0.32 (8/25) day per century. This is what lunar_adj does: Increase the Metonic year length to match the Julian calendar.

Also, recall that the Gregorian calendar removed three leap days every 400 years, reducing the average year length to 365.2425 days. This is what solar_adj does.

leap_months is a slight adjustment to the Julian calendar's 7/19 leap year cycle. Note that when total_adj reaches 30 days, an extra leap month is added.

The full_moon calculation is very similar to the Julian one, with a non-leap (12-month) lunar year of 6725/19 days, and 30-day leap months. But there's a lunar adjustment, and a change to the constant offset.

The if statement is...difficult to explain, but necessary for the calculation to work.

Anyhow, I've verified my function against the dates at https://www.projectpluto.com/easter.htm.

Ferromanganese answered 2/4, 2024 at 6:20 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.