The Doomsday algorithm
This year, my colleague Xavier told me about the doomsday algorithm. This algorithm is about being able to mentally compute the day of the week for a given date.
Although it wasn’t my plan to actually be able to do this, the subject became really interesting for me when I read the following on wikipedia:
It takes advantage of each year having a certain day of the week upon which certain easy-to-remember dates, called the doomsdays, fall; for example, the last day of February, April 4 (4/4), June 6 (6/6), August 8 (8/8), October 10 (10/10), and December 12 (12/12) all occur on the same day of the week in the year.
To get a better understanding about this fun fact, let’s think for a moment about the calendar and how it works.
Every year is either normal, meaning it has 365 days, or a leap year, meaning it has 366 days. The ordering of the months within the days is as follows:
month | days |
---|---|
January | 31 |
February | 28 or 29 (leap) |
March | 31 |
April | 30 |
May | 31 |
June | 30 |
July | 31 |
August | 31 |
September | 30 |
October | 31 |
November | 30 |
December | 31 |
This is all, conventional and there are a number of oddities:
- months alternate between 30 and 31 days
- but July and August have 31 days - they don’t alternate
- february is shorter than "usual months" and is changed by leap years
With all this in mind, let’s start by setting up a standard year. We can represent dates by a tuple of numbers, e.g. (3, 15) for the 15th of March.
year = []
months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
for month in range(len(months)):
days = months[month]
for day in range(days):
year.append((month + 1, day + 1))
year[:10], len(year)
([(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10)], 365)
Now, how do the days of the week come into play here? If we code the weekdays as
number | weekday |
---|---|
Sunday | 0 |
Monday | 1 |
Tuesday | 2 |
Wednesday | 3 |
Thursday | 4 |
Friday | 5 |
Saturday | 6 |
Then, we can assign the following weekdays to dates in the year, arbitrarily starting on a Sunday:
day_with_weekdays = []
weekday = 0
for day in year:
day_with_weekdays.append(day + (weekday,))
weekday = (weekday + 1) % 7
day_with_weekdays[:10]
[(1, 1, 0), (1, 2, 1), (1, 3, 2), (1, 4, 3), (1, 5, 4), (1, 6, 5), (1, 7, 6), (1, 8, 0), (1, 9, 1), (1, 10, 2)]
Of course, we could have started with another day as the first day of the year, but the aspect I want to highlight is that the relative distance between days does not change.
Now, let’s see if we can pick out patterns. To do see let’s lay the days out "spatially".
What I mean is that we can assigne 2D coordinates to each day of the year by setting the week number as the integer part of $d / 7$ and its row as the remainder of $d/7$.
def year2coords(year):
coords = {}
weekday = 0
for day in year:
col = weekday // 7
row = weekday % 7
coords[(row, col)] = f"{day[0]}:{day[1]}"
weekday+= 1
return coords
coords = year2coords(year)
Now, we can build an HTML table with this data.
from IPython.display import HTML
def make_html(coords, title, highlight_row=2):
html = f"<h1>{title}</h1><table>"
for row in range(7):
if row == highlight_row:
prefix = "<b>"
suffix = "</b>"
else:
prefix, suffix = "", ""
html += "<tr>"
for col in range(53):
html += f"<td>{prefix}{coords.get((row, col), "")}{suffix}</td>"
html += "</tr>"
html += "</table>"
return html
HTML(make_html(coords, 'normal year'))
normal year
1:1 | 1:8 | 1:15 | 1:22 | 1:29 | 2:5 | 2:12 | 2:19 | 2:26 | 3:5 | 3:12 | 3:19 | 3:26 | 4:2 | 4:9 | 4:16 | 4:23 | 4:30 | 5:7 | 5:14 | 5:21 | 5:28 | 6:4 | 6:11 | 6:18 | 6:25 | 7:2 | 7:9 | 7:16 | 7:23 | 7:30 | 8:6 | 8:13 | 8:20 | 8:27 | 9:3 | 9:10 | 9:17 | 9:24 | 10:1 | 10:8 | 10:15 | 10:22 | 10:29 | 11:5 | 11:12 | 11:19 | 11:26 | 12:3 | 12:10 | 12:17 | 12:24 | 12:31 |
1:2 | 1:9 | 1:16 | 1:23 | 1:30 | 2:6 | 2:13 | 2:20 | 2:27 | 3:6 | 3:13 | 3:20 | 3:27 | 4:3 | 4:10 | 4:17 | 4:24 | 5:1 | 5:8 | 5:15 | 5:22 | 5:29 | 6:5 | 6:12 | 6:19 | 6:26 | 7:3 | 7:10 | 7:17 | 7:24 | 7:31 | 8:7 | 8:14 | 8:21 | 8:28 | 9:4 | 9:11 | 9:18 | 9:25 | 10:2 | 10:9 | 10:16 | 10:23 | 10:30 | 11:6 | 11:13 | 11:20 | 11:27 | 12:4 | 12:11 | 12:18 | 12:25 | |
1:3 | 1:10 | 1:17 | 1:24 | 1:31 | 2:7 | 2:14 | 2:21 | 2:28 | 3:7 | 3:14 | 3:21 | 3:28 | 4:4 | 4:11 | 4:18 | 4:25 | 5:2 | 5:9 | 5:16 | 5:23 | 5:30 | 6:6 | 6:13 | 6:20 | 6:27 | 7:4 | 7:11 | 7:18 | 7:25 | 8:1 | 8:8 | 8:15 | 8:22 | 8:29 | 9:5 | 9:12 | 9:19 | 9:26 | 10:3 | 10:10 | 10:17 | 10:24 | 10:31 | 11:7 | 11:14 | 11:21 | 11:28 | 12:5 | 12:12 | 12:19 | 12:26 | |
1:4 | 1:11 | 1:18 | 1:25 | 2:1 | 2:8 | 2:15 | 2:22 | 3:1 | 3:8 | 3:15 | 3:22 | 3:29 | 4:5 | 4:12 | 4:19 | 4:26 | 5:3 | 5:10 | 5:17 | 5:24 | 5:31 | 6:7 | 6:14 | 6:21 | 6:28 | 7:5 | 7:12 | 7:19 | 7:26 | 8:2 | 8:9 | 8:16 | 8:23 | 8:30 | 9:6 | 9:13 | 9:20 | 9:27 | 10:4 | 10:11 | 10:18 | 10:25 | 11:1 | 11:8 | 11:15 | 11:22 | 11:29 | 12:6 | 12:13 | 12:20 | 12:27 | |
1:5 | 1:12 | 1:19 | 1:26 | 2:2 | 2:9 | 2:16 | 2:23 | 3:2 | 3:9 | 3:16 | 3:23 | 3:30 | 4:6 | 4:13 | 4:20 | 4:27 | 5:4 | 5:11 | 5:18 | 5:25 | 6:1 | 6:8 | 6:15 | 6:22 | 6:29 | 7:6 | 7:13 | 7:20 | 7:27 | 8:3 | 8:10 | 8:17 | 8:24 | 8:31 | 9:7 | 9:14 | 9:21 | 9:28 | 10:5 | 10:12 | 10:19 | 10:26 | 11:2 | 11:9 | 11:16 | 11:23 | 11:30 | 12:7 | 12:14 | 12:21 | 12:28 | |
1:6 | 1:13 | 1:20 | 1:27 | 2:3 | 2:10 | 2:17 | 2:24 | 3:3 | 3:10 | 3:17 | 3:24 | 3:31 | 4:7 | 4:14 | 4:21 | 4:28 | 5:5 | 5:12 | 5:19 | 5:26 | 6:2 | 6:9 | 6:16 | 6:23 | 6:30 | 7:7 | 7:14 | 7:21 | 7:28 | 8:4 | 8:11 | 8:18 | 8:25 | 9:1 | 9:8 | 9:15 | 9:22 | 9:29 | 10:6 | 10:13 | 10:20 | 10:27 | 11:3 | 11:10 | 11:17 | 11:24 | 12:1 | 12:8 | 12:15 | 12:22 | 12:29 | |
1:7 | 1:14 | 1:21 | 1:28 | 2:4 | 2:11 | 2:18 | 2:25 | 3:4 | 3:11 | 3:18 | 3:25 | 4:1 | 4:8 | 4:15 | 4:22 | 4:29 | 5:6 | 5:13 | 5:20 | 5:27 | 6:3 | 6:10 | 6:17 | 6:24 | 7:1 | 7:8 | 7:15 | 7:22 | 7:29 | 8:5 | 8:12 | 8:19 | 8:26 | 9:2 | 9:9 | 9:16 | 9:23 | 9:30 | 10:7 | 10:14 | 10:21 | 10:28 | 11:4 | 11:11 | 11:18 | 11:25 | 12:2 | 12:9 | 12:16 | 12:23 | 12:30 |
As one can see in the above diagram, the highlighted line shows the patterns that are used for the doomsday algorithm. These days are used as markers to compute the weekday for dates relative to them. These are the so-called doomsdays.
Some of them nicely fit into the following mnemonics:
- 4/4, 6/6, 8/8, 10/10, 12/12
- "working 9-5 at 7-11" gives the pairs for 9/5, 5/9, 7/11, 11/7
January and February pose a particular problem due to the possibily of leap years, which disturb the ordering for the days before the leap day. Let’s see how this modifies the table.
leap_year = []
leap_months = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
for month in range(len(leap_months)):
days = leap_months[month]
for day in range(days):
leap_year.append((month + 1, day + 1))
len(leap_year)
366
leap_coords = year2coords(leap_year)
HTML(make_html(leap_coords, 'leap_year', highlight_row=3))
leap_year
1:1 | 1:8 | 1:15 | 1:22 | 1:29 | 2:5 | 2:12 | 2:19 | 2:26 | 3:4 | 3:11 | 3:18 | 3:25 | 4:1 | 4:8 | 4:15 | 4:22 | 4:29 | 5:6 | 5:13 | 5:20 | 5:27 | 6:3 | 6:10 | 6:17 | 6:24 | 7:1 | 7:8 | 7:15 | 7:22 | 7:29 | 8:5 | 8:12 | 8:19 | 8:26 | 9:2 | 9:9 | 9:16 | 9:23 | 9:30 | 10:7 | 10:14 | 10:21 | 10:28 | 11:4 | 11:11 | 11:18 | 11:25 | 12:2 | 12:9 | 12:16 | 12:23 | 12:30 |
1:2 | 1:9 | 1:16 | 1:23 | 1:30 | 2:6 | 2:13 | 2:20 | 2:27 | 3:5 | 3:12 | 3:19 | 3:26 | 4:2 | 4:9 | 4:16 | 4:23 | 4:30 | 5:7 | 5:14 | 5:21 | 5:28 | 6:4 | 6:11 | 6:18 | 6:25 | 7:2 | 7:9 | 7:16 | 7:23 | 7:30 | 8:6 | 8:13 | 8:20 | 8:27 | 9:3 | 9:10 | 9:17 | 9:24 | 10:1 | 10:8 | 10:15 | 10:22 | 10:29 | 11:5 | 11:12 | 11:19 | 11:26 | 12:3 | 12:10 | 12:17 | 12:24 | 12:31 |
1:3 | 1:10 | 1:17 | 1:24 | 1:31 | 2:7 | 2:14 | 2:21 | 2:28 | 3:6 | 3:13 | 3:20 | 3:27 | 4:3 | 4:10 | 4:17 | 4:24 | 5:1 | 5:8 | 5:15 | 5:22 | 5:29 | 6:5 | 6:12 | 6:19 | 6:26 | 7:3 | 7:10 | 7:17 | 7:24 | 7:31 | 8:7 | 8:14 | 8:21 | 8:28 | 9:4 | 9:11 | 9:18 | 9:25 | 10:2 | 10:9 | 10:16 | 10:23 | 10:30 | 11:6 | 11:13 | 11:20 | 11:27 | 12:4 | 12:11 | 12:18 | 12:25 | |
1:4 | 1:11 | 1:18 | 1:25 | 2:1 | 2:8 | 2:15 | 2:22 | 2:29 | 3:7 | 3:14 | 3:21 | 3:28 | 4:4 | 4:11 | 4:18 | 4:25 | 5:2 | 5:9 | 5:16 | 5:23 | 5:30 | 6:6 | 6:13 | 6:20 | 6:27 | 7:4 | 7:11 | 7:18 | 7:25 | 8:1 | 8:8 | 8:15 | 8:22 | 8:29 | 9:5 | 9:12 | 9:19 | 9:26 | 10:3 | 10:10 | 10:17 | 10:24 | 10:31 | 11:7 | 11:14 | 11:21 | 11:28 | 12:5 | 12:12 | 12:19 | 12:26 | |
1:5 | 1:12 | 1:19 | 1:26 | 2:2 | 2:9 | 2:16 | 2:23 | 3:1 | 3:8 | 3:15 | 3:22 | 3:29 | 4:5 | 4:12 | 4:19 | 4:26 | 5:3 | 5:10 | 5:17 | 5:24 | 5:31 | 6:7 | 6:14 | 6:21 | 6:28 | 7:5 | 7:12 | 7:19 | 7:26 | 8:2 | 8:9 | 8:16 | 8:23 | 8:30 | 9:6 | 9:13 | 9:20 | 9:27 | 10:4 | 10:11 | 10:18 | 10:25 | 11:1 | 11:8 | 11:15 | 11:22 | 11:29 | 12:6 | 12:13 | 12:20 | 12:27 | |
1:6 | 1:13 | 1:20 | 1:27 | 2:3 | 2:10 | 2:17 | 2:24 | 3:2 | 3:9 | 3:16 | 3:23 | 3:30 | 4:6 | 4:13 | 4:20 | 4:27 | 5:4 | 5:11 | 5:18 | 5:25 | 6:1 | 6:8 | 6:15 | 6:22 | 6:29 | 7:6 | 7:13 | 7:20 | 7:27 | 8:3 | 8:10 | 8:17 | 8:24 | 8:31 | 9:7 | 9:14 | 9:21 | 9:28 | 10:5 | 10:12 | 10:19 | 10:26 | 11:2 | 11:9 | 11:16 | 11:23 | 11:30 | 12:7 | 12:14 | 12:21 | 12:28 | |
1:7 | 1:14 | 1:21 | 1:28 | 2:4 | 2:11 | 2:18 | 2:25 | 3:3 | 3:10 | 3:17 | 3:24 | 3:31 | 4:7 | 4:14 | 4:21 | 4:28 | 5:5 | 5:12 | 5:19 | 5:26 | 6:2 | 6:9 | 6:16 | 6:23 | 6:30 | 7:7 | 7:14 | 7:21 | 7:28 | 8:4 | 8:11 | 8:18 | 8:25 | 9:1 | 9:8 | 9:15 | 9:22 | 9:29 | 10:6 | 10:13 | 10:20 | 10:27 | 11:3 | 11:10 | 11:17 | 11:24 | 12:1 | 12:8 | 12:15 | 12:22 | 12:29 |
As the above shows, the addition of the 29th of February shifts the special row down by one. This enables us to find the rule for January and February.
- normal year: 1/3 and 2/28
- leap year: 1/4 and 2/29
The above can be simplified for February to "the last day of february".
Another relevant observation relates to the 0th day of several months. The 0th of March is a doomsday, which is a nice thing because this simplifies the computation of a weekday in March to just modulo 7 of the days. This is also true for the 0th of November. And for the 0th of February but only in normal years.
So finally, here’s my own list of useful doomsdays.
- January: 1/3 (normal) or 1/4 leap year
- February: 2/0 and 2/28 (last day) (normal) or 2/1 and 2/29 (last day) leap year
- March: 3/0 and 3/14
- April: 4/4
- May: 5/9
- June: 6/6
- July: 7/11
- August: 8/8
- Septembre: 9/5
- October: 10/10
- November: 11/0 and 11/7
- December: 12/12
Now, if you want to try and have fun, you can check out my colleague Xavier’s implementation here: https://xavartley.github.io/#misc/guess_the_weekday.html
This post was entirely written using the Jupyter Notebook. Its content is BSD-licensed. You can see a static view or download this notebook with the help of nbviewer at 20250904_doomsday_algorithm.ipynb.