Calculating Easter

While working on a script to tell me the date of the next fortnightly recycling collection in my area, I’ve had cause to research the calculation of Easter Day. (Easter Monday’s a bank holiday, so any collections that week will be a day later.) It’s quite a complex process, based on full moons and Vernal Equinoxes. One of the calculations I found on the Web actually works and looks like this when translated into AppleScript:

on EasterDay(y) -- y is the year number.
	-- Based on a calculation on the BBC's h2g2 Web site:
	-- <http://www.bbc.co.uk/dna/h2g2/A653267>
	
	set a to y mod 19
	set b to y div 100
	set c to y mod 100
	set d to b div 4
	set e to b mod 4
	set f to c div 4
	set g to c mod 4
	
	set h to (b + 8) div 25
	set i to (b - h + 1) div 3
	set j to (19 * a + b - d - i + 15) mod 30
	set k to (32 + 2 * e + 2 * f - j - g) mod 7
	set m to (a + 11 * j + 22 * k) div 451
	set n to j + k - 7 * m + 114
	
	set theDay to n mod 31 + 1
	set theMonth to n div 31
	
	return {theDay, theMonth}
end EasterDay

EasterDay(2006)
--> {16, 4} -- Sunday 16th April

This is apparently only guaranteed for years between 1700 and 2299! For my own purposes, I want the result as an AppleScript date.

on EasterDay(y)
	-- Based on a calculation on the BBC's h2g2 Web site:
	-- <http://www.bbc.co.uk/dna/h2g2/A653267>
	
	set {a, b, c} to {y mod 19, y div 100, y mod 100}
	
	set j to (19 * a + b - b div 4 - (b - (b + 8) div 25 + 1) div 3 + 15) mod 30
	set k to (32 + 2 * (b mod 4 + c div 4) - j - c mod 4) mod 7
	set n to j + k - (a + 11 * j + 22 * k) div 451 * 7 + 114
	
	-- Get 1st January in the target year.
	set theDate to date "Wednesday 1 January 1000 00:00:00"
	set theDate's year to y
	
	tell theDate + (n div 31 - 1) * 32 * days -- Get a date in the (n div 31)th month of the year.
		return it + (n mod 31 + 1 - (its day)) * days -- Return the result + (target day - actual day) days.
	end tell
end EasterDay

EasterDay(2006)
--> date "Sunday 16 April 2006 00:00:00"

This is a different approach, found here: http://www.assa.org.au/edm.html
The dates are valid from 1583 to 4099 and independent of international system date settings.
For further determination of holidays I’ve added a few lines to calculate the days of february


set {EasterDate, DaysOfFeb} to calc_easter_date(2006)

on calc_easter_date(yr)
	set FirstD to yr div 100
	set temp to (FirstD - 15) div 2 + 202 - 11 * (yr mod 19)
	
	if FirstD is in {21, 24, 25, 27, 28, 29, 30, 31, 32, 34, 35, 38} then set temp to temp - 1
	if FirstD is in {33, 36, 37, 39, 40} then set temp to temp - 2
	set temp to temp mod 30
	
	set VA to temp + 21
	if temp = 29 or (temp = 28 and (yr mod 19) > 10) then set VA to VA - 1
	
	-- find the next Sunday
	set VB to (VA - 19) mod 7
	set VC to (40 - FirstD) mod 4
	if VC = 3 then set VC to VC + 1
	if VC > 1 then set VC to VC + 1
	set temp to yr mod 100
	set VD to (temp + temp div 4) mod 7
	set VE to ((20 - VB - VC - VD) mod 7) + 1
	set easter_day to VA + VE
	
	-- return the date
	if easter_day > 31 then
		set easter_day to easter_day - 31
		set easter_month to 4
	else
		set easter_month to 3
	end if
	-- calculate days of february
	set DayFeb to 28
	if ((yr - (yr div 4) * 4) = 0) and (yr - ((yr div 100) * 100) ≠ 0) then set DayFeb to 29
	if (yr - (yr div 100) * 100) = 0 and yr - (yr div 400) * 400 ≠ 0 then set DayFeb to 28
	if (yr - (yr div 400) * 400) = 0 then set DayFeb to 29
	-- make the date independent of international system date settings
	set ED to date (short date string of (current date))
	set year of ED to yr
	set month of ED to easter_month
	set day of ED to easter_day
	return {ED, DayFeb}
end calc_easter_date

Model: G5 dual 2,5 GHz
Browser: Safari 419.3
Operating System: Mac OS X (10.4)

Hi StefanK.

In your translation of their algorithm, it seems you have made an error! :slight_smile:

You have written:

if temp = 29 or (temp = 28 and (yr mod 19) > 10) then VA = VA - 1

That last bit should be:

then set VA to VA - 1

Out of interest, off that website I found Modified Oudin’s Algorithm, which I haven’t tried simplifying (but have verified that it works):

on modifiedOudinsAlgorithm(theYear)
	(* Modified Oudin's Algorithm *)
	(* http://www.smart.net/~mmontes/oudin.html *)
	set century to theYear div 100
	set G to theYear mod 19
	set K to (century - 17) div 25
	set I to (century - century div 4 - (century - K) div 3 + 19 * G + 15) mod 30
	set I to I - (I div 28) * (1 - (I div 28) * (29 div (I + 1)) * ((21 - G) div 11))
	set J to (theYear + theYear div 4 + I + 2 - century + century div 4) mod 7
	set L to I - J
	set EasterMonth to 3 + (L + 40) div 44
	set EasterDay to L + 28 - 31 * (EasterMonth div 4)
	
	set EasterDate to current date
	tell EasterDate to set {day, its month, year, time} to {EasterDay, EasterMonth, theYear, 0}
	return EasterDate
end modifiedOudinsAlgorithm

I also found Butcher’s Algorithm, which also gets the same result as yours:

on butchersAlgorithm(theYear)
	(* Butcher's Algorithm *)
	(* http://www.smart.net/~mmontes/nature1876.html *)
	set a to theYear mod 19
	set b to theYear div 100
	set c to theYear mod 100
	set d to b div 4
	set e to b mod 4
	set f to (b + 8) div 25
	set G to (b - f + 1) div 3
	set h to (19 * a + b - d - G + 15) mod 30
	set I to c div 4
	set K to c mod 4
	set L to (32 + 2 * e + 2 * I - h - K) mod 7
	set m to (a + 11 * h + 22 * L) div 451
	set EasterMonth to (h + L - 7 * m + 114) div 31
	set p to (h + L - 7 * m + 114) mod 31
	set EasterDay to p + 1
	
	set EasterDate to current date
	tell EasterDate to set {day, its month, year, time} to {EasterDay, EasterMonth, theYear, 0}
	return EasterDate
end butchersAlgorithm

Also, to find how many days in February for a particular year:

on daysInFeb(theYear)
	tell theYear mod 400 to return 28 + (((it mod 28 mod 4 is 0) and ((it mod 100 is not 0) or (it is 0))) as integer)
end daysInFeb

Edit
Look, there are reasons why you don’t attempt to condense these into a one-liner form:

on butchersAlgorithmButchered(theYear)
	tell {theYear mod 19, theYear div 100} to tell {item 1, item 2, theYear mod 100, (19 * (item 1) + (item 2) - (item 2) div 4 - (((item 2) - (((item 2) + 8) div 25) + 1) div 3) + 15) mod 30} to tell {item 1, item 4, (32 + 2 * ((item 2) mod 4) + 2 * ((item 3) div 4) - (item 4) - ((item 3) mod 4)) mod 7} to tell ((item 2) + (item 3) - 7 * (((item 1) + 11 * (item 2) + 22 * (item 3)) div 451) + 114) to set {EasterMonth, EasterDay} to {it div 31, it mod 31 + 1}
	
	set EasterDate to current date
	tell EasterDate to set {day, its month, year, time} to {EasterDay, EasterMonth, theYear, 0}
	return EasterDate
end butchersAlgorithmButchered

Hi, Qwerty

thanks a lot for your hint. I’ve changed the line.

Your handler to calculate leap years doesn’t work correctly for century years.
The rule is:

A year will be a leap year if it is divisible by 4 but not by 100.
If a year is divisible by 4 and by 100, it is not a leap year unless it is also divisible by 400.

therefore e.g. 1900 is not a leap year but 2000 is.

You are of course correct.

Hmm… I honestly remembered as being the other way around. :confused: Makes much more sense though.
Consider it changed.

Consider it optimised. :wink:

on daysInFeb(theYear)
	if ((theYear mod 4 is 0) and ((theYear mod 100 > 0) or (theYear mod 400 is 0))) then
		29
	else
		28
	end if
end daysInFeb

almost :wink:

on daysInFeb(theYear)
	(((theYear mod 4 is 0) and ((theYear mod 100 > 0) or (theYear mod 400 is 0))) as integer) + 28
end daysInFeb

Hi, Stefan. :slight_smile:

By “optimised”, I meant “made more efficient”, not just “made shorter”. I deliberately left out the boolean-to-integer coercion and the addition, which makes my version about 2.5 to 3.7 times as fast (on my machine, depending on the actual year number) as a one-liner.

Hi Nigel,

I’m convinced :slight_smile:

BTW: How can you quantify these tiny durations?
Script Debugger shows in both cases 0,0 secs

Me too. :wink:

This is how I might go about measuring the speed difference:

on daysInFeb1(theYear)
	if ((theYear mod 4 is 0) and ((theYear mod 100 > 0) or (theYear mod 400 is 0))) then
		29
	else
		28
	end if
end daysInFeb1

on daysInFeb2(theYear)
	(((theYear mod 4 is 0) and ((theYear mod 100 > 0) or (theYear mod 400 is 0))) as integer) + 28
end daysInFeb2

set repeatTimes to 1000

set startDate1 to current date
repeat repeatTimes times
	repeat with i from 1 to 2099
		daysInFeb1(i)
	end repeat
end repeat
set time1 to (current date) - startDate1

set startDate2 to current date
repeat repeatTimes times
	repeat with i from 1 to 2099
		daysInFeb2(i)
	end repeat
end repeat
set time2 to (current date) - startDate2

time2 / time1 -- times faster

However, I have heard that AppleScript can be quite fragile when doing timings, and results can change depending on the order and the number of times you run it. I certainly get small discrepancies in the order of about 5 to 10 percent, but in general it’s usually quite clear as to which one is faster.

thanx,

such a doddle :wink:

QD’s test results in 2.888888888889 on mm (just for the record)

Similar average result here, timing both with current date and with GetMilliSec, which is a sharper instrument. But the tests aren’t actually comparing the timings of 2099000 calls to the two handlers, they’re testing 1000 executions of the inner 2099-repeat loops “ which include the handler calls. In other words, both test times include 1000 settings-up of the inner repeats + 2099 * 1000 iterations of the inner looping code + 1 setting-up of the outer repeat + 1000 iterations of the outer looping code. Subtracting these from the total times and assuming the repeat instructions take exactly the same time in both cases, we’re left with smaller timings but the same difference, which suggests an even better ratio between the handlers themselves. Eschewing the outer repeats and having the inner repeats loop from 1 to 2099000, I see a small (about 1.74%) but definite improvement in the comparative figure.

Running the nested repeats empty (without the handler calls) takes about 1.34 seconds each on my machine. That’s added to the timing of the test for each handler when I run Qwerty’s script. Running non-nested 1-to-2099000 repeats by themselves takes about 1.09 seconds. Empty, non-nested ‘2099000 times’ repeats take only about 0.6 seconds, so they distort the comparative speeds the least. But in the current case, the 1-to-2099000 repeats are possibly preferable since they give a better average over the various numbers that can be passed to the handlers. This is necessary because the handlers themselves do more or fewer boolean tests depending on the year number they’re given, so these tests are a greater or lesser proportion of their total running times, which in turn affects their relative running times. :slight_smile: Repeating ‘2099000 times’ with a year number of 1999 (only the first boolean test is performed in each handler) gives me a relative time of 3.05, whereas with 2000 (all the tests are performed), it’s reduced to 2.3.

Moral:

I should get an early night. :wink:

Hello.

The ecclestical easter (which we base our hollidays on), differs from the astronomical easter.

This one returns the date of easter by ecclestical standards.

to MonthAndDayOfEaster for aYear
# http://aa.usno.navy.mil/faq/docs/easter.php
	set c to aYear div 100
	set n to aYear - 19 * (aYear div 19)
	set k to (c - 17) div 25
	set i to c - c div 4 - (c - k) div 3 + 19 * n + 15
	set i to i - 30 * (i div 30)
	set i to i - (i div 28) * (1 - (i div 28) * (29 div (i + 1)) * ((21 - n) div 11))
	set j to aYear + aYear div 4 + i + 2 - c + c div 4
	set j to j - 7 * (j div 7)
	set l to i - j
	set m to 3 + (l + 40) div 44
	set d to l + 28 - 31 * (m div 4)
	return {m, d}
end MonthAndDayOfEaster

Edit

In all fairness: in our particular time span, between 1982 and 2037, the astronomical easter and ecclestical easter do coincide, I just posted this, in case one wants to figure out earlier, and later! easter dates, for other purposes than calculating lunar phases. :slight_smile:

Hi all! What about this kind of code?

display dialog "Easter : " & (do shell script "LC_TIME=\"fr_BE.UTF-8\" ncal -e \"2014\"")

Hello.

Thanks for sharing! I read that ncal even has an option for calculating eastor for orthodox/Eastern churches, (-o) It’s impressive! :slight_smile: