Part 2 of Algorithms explained! Every few weeks I write about an algorithm and explain and implement it!
Is there an algorithm you always wanted to know about? Leave a comment!
Today: The Doomsday rule - or: figuring out if November 24th 1763 was a Tuesday
John Conway is most famously known for "Conway's Game Of Life" - but he published a whole lot more. One especially remarkable algorithm he developed around 1970 is the Doomsday rule.
The name may sound a little odd, but the algorithm is amazing: It allows you to calculate the day of the week for any given date in your head.
In this post I will explain how this algorithm works and do a practical example in PHP.
What's a Doomsday, though?
The "Doomsday" of the "Doomsday rule" is not about the end of the world. Every year has so called Doomsdays. A Doomsday is a day whose weekday is known. For every year the Doomsdays have the same weekday.
For example: If you know that the November 7th (a Doomsday) was a Saturday - which weekday is November 21st? You know that there's 14 days in between and you know that a week has 7 days - so it's , so 2 whole weeks. November 21st must therefore be a Saturday as well.
To make calculations easier, each month has a fixed Doomsday. This table helps to memorize them. There's also some memory hooks, like "I work 9-to-5 at 7-11".
Month | Date of Doomsday | Memory hook |
---|---|---|
January | 3rd (4th in leap years) | 3 years 3, 4 in 4 |
February | 28th (29th in leap years) | Last day of Feb |
March | 0th (yes.) | Division by 0 |
April | 4th | 4/4 |
May | 9th | "... 9 to 5 ..." |
June | 6th | 6/6 |
July | 11th | "... at 7-11" |
August | 8th | 8/8 |
September | 5th | "... 9 to 5 ..." |
October | 10th | 10/10 |
November | 7th | "... at 7-11" |
December | 12th | 12/12 |
Let's have a look at a calendar:
And indeed: All these dates have the same weekday. Actually, all dates that have the same weekday can be considered Doomsdays.
The algorithm
The Doomsday rule takes advantage of the fact that patterns in calendars repeat every so often. Here's a basic diagram of what the algorithm is actually doing:
It basically narrows down the weekday by first looking at the century given, then the year, then the month and finally pinponting the exact date. Let's go through it step-by-step. We'll try to figure out the weekday of November 24th, 1763 (I'll explain why I chose this particular date later on).
Figuring out the weekday of the Doomsdays
To figure out the weekday of the Doomsdays of a given year (the so called "Anchor day"), we need to know the so called "Century Anchor day" first. This is the Anchor day of the first year of a given century. Those repeat in a 400 year cycle:
Year | Anchor day |
---|---|
1400 | Friday |
1500 | Wednesday |
1600 | Tuesday |
1700 | Sunday |
1800 | Friday |
1900 | Wednesday |
2000 | Tuesday |
2100 | Sunday |
2200 | Friday |
2300 | Wednesday |
2400 | Tuesday |
If we calculate that back to the year 0, we get a Tuesday. This we can use to calculate the Anchor day for any century. First, let's label the weekdays, starting from Sunday:
$weekdays = [
0 => 'Sunday',
1 => 'Monday',
2 => 'Tuesday',
3 => 'Wednesday',
4 => 'Thursday',
5 => 'Friday',
6 => 'Saturday',
];
Then, take the century (so essentially ) (to get into the 4 century cycle) and multiply that by 2. Next we take the Tuesday of year 0 into account by subtracting the 4 century cycle number from it (i.e. ) Since this often gives negative results, we use 9 (Tuesday + 1 week) instead and take the result to get rid of that extra week.
(By the way: We're going to use a lot of %
today.)
Let's see if this works:
Century | Target Anchor day | mod 4 | * 2 | 9 - | % 7 |
---|---|---|---|---|---|
0 | Tue / 2 | 0 | 0 | 9 | 2 |
1 | Sun / 0 | 1 | 2 | 7 | 0 |
2 | Fri / 5 | 2 | 4 | 5 | 5 |
3 | Wed / 3 | 3 | 6 | 3 | 3 |
4 | Tue / 2 | 0 | 0 | 9 | 2 |
5 | Sun / 0 | 1 | 2 | 7 | 0 |
6 | Fri / 5 | 2 | 4 | 5 | 5 |
7 | Wed / 3 | 3 | 6 | 3 | 3 |
8 | Tue / 2 | 0 | 2 | 9 | 2 |
9 | Sun / 0 | 1 | 4 | 7 | 0 |
The 4 century cycle works because every 400 years, calendars basically repeat. For every year of a century, the Doomsday weekday can be calculated from the century it is in.
Let's code this out:
/**
* Determines the anchor day a century.
*
* @param int $yyyy Year, 1-4 digits
* @return int Anchor day number
*/
function getCenturyAnchorday(int $yyyy): int {
return (9 - (floor($yyyy / 100) % 4) * 2) % 7;
}
So the Century anchor day for November 24th 1763 is (9 - (floor(1763 / 100) % 4) * 2) % 7
which is 0
, so Sunday
.
From century to year
With the Century Anchor day ready, we can calculate the Anchor day for the year itself. We first need the last two digits of the year: $year % 100
. The weekdays for the Year Anchor days are basically increasing, so Tue, Wed, Thu, Fri, Sat, Sun, Mon, Tue, except for leap years, where a day is skipped (since there's a day more).
Let's make an example with the first few years of the 17th century (1600 onwards):
Year | Anchor day |
---|---|
1600 (leap) | Tuesday |
1601 | Wednesday |
1602 | Thursday |
1603 | Friday |
1604 (leap) | Sunday |
1605 | Monday |
1606 | Tuesday |
1607 | Wednesday |
1608 (leap) | Friday |
1609 | Saturday |
1610 | Sunday |
1611 | Monday |
1612 (leap) | Wednesday |
1613 | Thursday |
1614 | Friday |
1615 | Saturday |
This result can be achieved by taking the year, adding the number of leap years since the century started (i.e. the number of additional days to count in) and adding the Century Anchor day. This result mod 7 is then the Year Anchor day. This is what the code looks like:
/**
* Determines the year's anchor day.
*
* @param int $yyyy Year, 1-4 digits
* @return int Year anchor day
*/
function getYearAnchorDay(int $yyyy): int {
$centuryAnchorday = getCenturyAnchorday($yyyy);
$yy = $yyyy % 100; // Year, 1-2 digits
return ($yy + floor($yy / 4) + $centuryAnchorday) % 7;
}
Since we know that the Century Anchor day of November 24th 1763 is 0
, we can calculate the Year Anchor day: (63 + floor(63 / 4) + 0) % 7
which is 1
, so Monday
.
Marvelous! We can now determine the weekday of the Doomsdays of any given year! Next up is pinpointing the actual day by finding the next best Doomsday. With our Doomsday table above we can make this a simple mapping:
/**
* Determines if a given year is a leap year.
*
* @param int $year
* @return bool
*/
function isLeapYear(int $year): bool {
return $year % 4 === 0 && ($year % 100 !== 0 || $year % 400 === 0);
}
/**
* Determines the Doomsday of a given month.
*
* @param int $yyyy Year, 1-4 digits
* @param int $m Month, 1-2 digits
* @return int
*/
function getNearestDoomsday(int $yyyy, int $m): int {
$isLeapYear = isLeapYear($yyyy);
return [
1 => !$isLeapYear ? 3 : 4,
2 => !$isLeapYear ? 28 : 29,
3 => 0,
4 => 4,
5 => 9,
6 => 6,
7 => 11,
8 => 8,
9 => 5,
10 => 10,
11 => 7,
12 => 12,
][$m];
}
1763 is not a leap year, and we're looking at November anyways, so the next best Doomsday is November 7th
.
Almost there!
Now we need to count the number of days from the actual date we're looking for to the next best anchor day. We calculate the difference between the date of the nearest Doomsday and the date we're looking for, add the Year Anchor day, add another 35 ( , to avoid any negative numbers) and take the result mod 7. This gives us the final weekday number!
For example, if the next best Doomsday is November 7th, and we're looking for November 24th, the calculation looks like this. We know that the Year Anchor day is a Monday, so 1. We also know the nearest Doomsday, which is November 7th:
What | Value | Result |
---|---|---|
Date | 24 | 24 |
- Doomsday | - 7 | 17 |
+ Year anchor day | + 1 | 18 |
+ Offset | + 35 | 53 |
Figure out weekday | % 7 | 4 |
And in code:
/**
* Determines the weekday of a given date.
*
* @param int $yyyy Year, 1-4 digits
* @param int $m Month, 1-2 digits
* @param int $d Day, 1-2 digits
* @return int Number of the weekday, 0 = Sun, 6 = Sat
*/
function getWeekday(int $yyyy, int $m, int $d): int {
$doomsday = getNearestDoomsday($yyyy, $m);
$yearAnchorDay = getYearAnchorDay($yyyy);
return ($yearAnchorDay + ($d - $doomsday) + 35) % 7;
}
And now we can calculate the weekday for November 24th 1763:
$weekdays[getWeekday(1763, 11, 24)]; // "Thursday"
So, no, November 24th 1763 is not actually a Tuesday, but a Thursday. Let's write a test for that:
$result = true;
for ($i = 0; $i < 1000; $i++) {
$yyyy = mt_rand(100, 9999); // PHP's mktime will make a wraparound with any smaller years.
$m = mt_rand(1, 12);
$d = mt_rand(1, 27);
$result = $result && $weekdays[getWeekday($yyyy, $m, $d)] === date('l', mktime(0, 0, 0, $m, $d, $yyyy));
}
var_dump($result); // true
So PHP gives the same results as our implementation of the Doomsday rule. Here's a PHPSandbox with the entire code!
Takeaway thoughts
When I first heard about the Doomsday rule, I was baffled. It took me quite some time (and calculating) to understand it, but once I got it, I was amazed by its cleverness. The algorithm isn't too costly either, because it only uses algebra.
With some more memory hooks (especially memorizing Doomsdays and anchor days for several years) one can calculate any weekday in their head. Kind of a neat party trick, actually: "Give me any date, I'll tell you its weekday."
Oh, and about November 24th 1763: That's the date when Bayes' Theorem was first announced.
I write tech articles in my free time. If you enjoyed reading this post, consider buying me a coffee!
Top comments (0)