A leap year inequality
March 15, 2026 at 6:22 PM by Dr. Drang
I’ve been working my way through the fourth edition of Reingold and Dershowitz’s Calendrical Calculations, and I want to talk about something I learned.

It’s a simple inequality that initially appears in the first chapter of the book and gets used several times thereafter. Here it is:
It’s first presented as a way to figure out how leap years are distributed. In some calendars—not the Gregorian—there’s a repeating cycle of years in which years are leap years. If the leap years are distributed as evenly as possible, then the years in which satisfies the inequality are the leap years. The is a sort of offset that determines the position within the cycle associated with Year 0, and the operator represents modulo division. In Python, that’s the % operator.
It’s helpful to look at examples. Let’s say we have a 7-year cycle with 2 leap years and 5 normal years in each cycle. The leap years have to be either 3 or 4 years apart. Here’s an example showing three cycles:
1 1 1 1 1 1 1 1 1 1 2 2
Year 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
Type N N N L N N L N N N L N N L N N N L N N L
For this, , , and . You can plug the values into the inequality to show that it’s satisfied for years 4, 7, 11, 14, 18, 21, and so on.
Here’s a similar example. The only difference is the offset. In this case, , and the leap years are years 2, 5, 9, 12, 16, 19, and so on.
1 1 1 1 1 1 1 1 1 1 2 2
Year 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
Type N L N N L N N N L N N L N N N L N N L N N
A practical example of this formula is with the Hebrew calendar. It has a 19-year cycle () with 7 leap years (), and the offset is 11 years (). The current year is 5786, for which
so this is not a leap year. But for next year,
so it is a leap year and will have 13 months instead of 12. You can confirm this on any number of websites; the key is to note that 5787 has both an Adar I and an Adar II.
The Gregorian calendar has a 400-year cycle with 97 leap years, but those leap years are not distributed as evenly as possible, so the formula can’t be used. If it had been used, we’d have leap years typically every fourth year but occasionally every fifth year. Pope Gregory and his people must’ve thought that would be too tricky to deal with.
Surprisingly (to me, anyway), Reingold and Dershowitz do use this formula with the Gregorian calendar, but they use it with months instead of years. Think of the months in the Gregorian calendar as being either short or long. In a year, there are 5 short months and 7 long months, and they’re distributed like this:
1 1 1
1 2 3 4 5 6 7 8 9 0 1 2
Month J F M A M J J A S O N D
Length L S L S L S L L S L S L
The positions of the long months correspond to our inequality with , , and . Plug in those values, and you’ll see that the long months are 1, 3, 5, 7, 8, 10, and 12.
To calculate the day number within a year, it’s usually easiest to calculate the number of days in the preceding months and then add the day number within the current month. Today is March 15, so it’s Day of the year.
Instead of looping through the lengths of the preceding months, R&D use a formula based on our inequality to count the number of long months before the current month. That formula is
where the brackets without tops represent the floor function, i.e., the integer equal to or just below what’s inside the brackets.
Plugging in our values for , , and and doing some algebra, we get
This is the number of long months in the year before the current month .
If February had 30 days, the number of days in the months before the current month would be
So to get the (Gregorian) day of the year, R&D calculate the day number as if February had 30 days and then subtract (if necessary) to account for February’s deficiency. In Python, the code looks like this:
python:
def day_of_year(year, month, day):
year_day = (367 * month - 362) // 12 + day
if month <= 2:
return year_day
elif leap_year(year):
return year_day - 1
else:
return year_day - 2
where I’m assuming we already have a Boolean function leap_year to determine whether it’s a leap year or not. That’s not necessarily the most obvious code in the world, but it makes sense if you’ve gone through the derivation.
One last thing. Reingold is the co-author of a paper in which our inequality is connected to Euclid’s algorithm for calculating the greatest common divisor and Bresenham’s algorithm for plotting lines on bitmaps. Which is pretty cool.
Scientific American and Friday the 13th
March 13, 2026 at 6:48 PM by Dr. Drang
Scientific American has a fun little article today about the frequency of Friday the 13ths. It ends with this table,

and this true but overstated conclusion:
In other words, the 13th of a month will be a Friday more times than any other day of the week.
Well, yes, if you live to be 400 years old, you’ll see one more Friday the 13th than Wednesday the 13ths or Sunday the 13ths. Kind of a weird thing to focus on, though. I’m guessing you’ll have other worries by then.
But I shouldn’t be so snarky. A few years ago, I wrote a post that calculated the same set of Fri13 counts for a 400-year Gregorian cycle. I did the calculations in Mathematica and (of course) showed the code. Today, I did the same thing in Python,
python:
1: #!/usr/bin/env python
2:
3: from datetime import date
4:
5: f13s = [0]*7
6: for y in range(1800, 2200):
7: for m in range(1, 13):
8: wd = date(y, m, 13).weekday()
9: f13s[wd] += 1
10:
11: print(f13s)
and got a result of
[685, 685, 687, 684, 688, 684, 687]
for Monday through Sunday. This also matches the SciAm table.
Those of us who are alive now (and have realistic longevities) won’t live through any non-leap century years. For us, the calendar has and will repeat every 28 years (1461 weeks), and over every 28-year period in our lives, there will be 48 Fri13s, the same as the number of Mon13s, Tue13s, Wed13s, and so on.
Of course, few of us live exactly a multiple of 28 years. Personally, I’ve lived through 113 Fri13s so far, which is just under the number of Sun13s I’ve seen (114). So I’ve been lucky?
In a Friday the 13th post from way back in 2012, I talked about how Fri13s repeat within years because the number of days in certain month sequences is a multiple of 7. So if there’s a Fri13 in April, there will be another in July because
Apr + May + Jun
30 + 31 + 30 = 91
which is 13 weeks. The last time that happened was in 2018.
Similarly, if there’s a Fri13 in September, there will also be one in December because
Sep + Oct + Nov
30 + 31 + 30 = 91
That pair of Fri13s last happened in 2024.
There’s also an 8-month sequence that adds to a multiple of 7:
Mar + Apr + May + Jun + Jul + Aug + Sep + Oct
31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 = 245
So there will be another Fri13 in November of this year.
The sequences above happen every year. In non-leap years only—this year, for example—a Fri13 in February will be followed by one in March. In leap years only, a Fri13 in January will be followed by one in April. That last happened in 2012.
I covered all these repeated Fri13s in that 2012 post. Today, I learned of a new repeat that spans certain year boundaries. If there’s a Fri13 in December of a non-leap year that’s followed by a leap year, there will be a Fri13 in March of that following year. That last happened in December of 2019 and March of 2020.
Superstitious or not, you have to admit March of 2020 was pretty unlucky.
Port ability
March 10, 2026 at 7:53 PM by Dr. Drang
I read Jason Snell’s review of the MacBook Neo this morning and was struck by this section on its two USB-C ports:
So Apple has done the work to put two USB-C ports on the Neo—and those ports reveal a bit more of the struggles Apple had in building this computer. Both of the USB-C ports will let you charge (which is good, because there’s no MagSafe), but only the one that’s furthest back is a fully functional USB 3 port with support for driving an external display at 4K, 60 frames per second. The closer-in USB port only offers USB 2 speeds. (The good news is that Apple has built alerts into macOS that will warn you if the device you’ve plugged into the slow port would be better off plugged into the faster one, so you won’t be transferring files slowly unnecessarily.)
It reminded me of Christina Warren’s angry Mastodon post about the Neo’s ports:
I’m just going to say that pointing out flaws in $600 and $700 laptops and having people instinctively respond “it’s not for real users, these buyers don’t know/care” is gross. Why should we make the assumption that those buyers don’t still deserve better, or at least on par with the 6 year old product it’s replacing that sold at the same price. Stop assuming people who don’t spend $1000+ on tech are imbeciles or deserve subpar options. No one in 2026 deserves a laptop with a single USB 3 port.
My first thought upon reading Christina’s complaint last week was “Why should I care about the configuration of a computer I’ll never buy?” Kind of a Republican thought, I admit, and I’m not proud of it. But I have many reasons to be disappointed by Apple, and I need to conserve my outrage. Christina’s much younger than I am and has deeper outrage reserves.
Jason pointed out the biggest problem with the low-speed port in his next paragraph:
Honestly, I’m more disappointed by the fact that mismatched ports can lead to user frustration—no, not that port, the other one—than I am about the one slow USB port.
Many Neo users will be too young to remember the joy of flipping USB-A plugs back and forth before getting the orientation right. Apple has updated that wonderful experience for the USB-C age.
Lent and Lisp
February 25, 2026 at 9:13 AM by Dr. Drang
After writing last week’s post about the start of Ramadan and Chinese New Year, I expected to hear from people asking why I didn’t include the further coincidence of Ash Wednesday. I was surprised that the only such feedback I got was an email from TJ Luoma. It makes sense that Lent would be on TJ’s mind—it’s a big part of his business calendar—but I had an answer prepared, and I wrote him back with my reasons.
As I typed out the reply, though, the reasons seemed weaker. Yes, it’s true that the full moon that determines this year’s date of Easter (and therefore Ash Wednesday) isn’t part of the same lunation that determines the start of Ramadan and Chinese New Year, so there was an astronomical reason to keep Ash Wednesday out of the post. But it’s also true that both Ramadan and Lent represent periods of self-denial, so there’s a cultural connection.
Adding a new bit of Emacs Lisp code to what I’d already written to include a check for Ash Wednesday wouldn’t be hard, but another thought was buzzing in my head: switching from Emacs Lisp to Common Lisp. The ELisp calendar functions were written by Edward Reingold and Nachum Dershowitz, authors of the well-known Calendrical Calculations. That book includes a lot of code that isn’t in the Emacs implementation, code that does astronomical calculations I’d like to explore. So it seemed like a good idea to write a Ramadan/Lent/Chinese New Year script in Common Lisp and use the functions from the book.
The problem with that idea was that the Common Lisp code I downloaded from the Cambridge University Press site, calendar.l, didn’t work. I tried it in both SBCL and CLISP, and calling
(load "calendrical.l")
threw a huge number of errors. I was, it turned out, not the first to have run into this problem. The workarounds suggested there on Stack Overflow didn’t help. There’s a port to Clojure that apparently works, but I was reluctant to use Clojure and have to maintain both it and a Java Virtual Machine.
What I found, though, was that Reingold & Dershowitz’s code would load in CLISP with one simple change. After many lines of comments, the working part of calendar.l starts with these lines:
(in-package "CC4")
(export '(
acre
advent
akan-day-name
akan-day-name-on-or-before
[and so on for a few hundred lines]
yom-ha-zikkaron
yom-kippur
zone
))
Deleting these lines got me a file that would load without errors in CLISP,1 so I named the edited version calendar.lisp and saved it in my ~/lisp/ directory. I believe the problem with the unedited code has something to do with packages and namespaces, and if I keep using Common Lisp long enough, I may learn how to make a better fix. Until then, this will do.
With a working library of calendar code, I wrote the following script, ramadan-lent, to get the dates for which Ramadan 1 and Ash Wednesday correspond over a 500-year period:
1: #! /usr/bin/env clisp -q
2:
3: ;; The edited Calendrical Calculations code by Reingold and Dershowitz
4: (load "calendar.lisp")
5:
6: ;; Names
7: (setq
8: weekday-names
9: '("Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday")
10:
11: gregorian-month-names
12: '("January" "February" "March" "April" "May" "June"
13: "July" "August" "September" "October" "November" "December"))
14:
15: ;; Date string function
16: (defun gregorian-date-string (date)
17: (let ((g-date (gregorian-from-fixed date))
18: (weekday (day-of-week-from-fixed date)))
19: (format nil "~a, ~a ~d, ~d"
20: (nth weekday weekday-names)
21: (nth (1- (second g-date)) gregorian-month-names)
22: (third g-date)
23: (first g-date))))
24:
25: ;; Get today's (Gregorian) date.
26: (multiple-value-setq
27: (t-second t-minute t-hour t-day t-month t-year t-weekday t-dstp t-tz)
28: (get-decoded-time))
29:
30: ;; Loop through 500 Islamic years, from 250 years ago to 250 years in
31: ;; the future and find each Ramadan 1 that corresponds to Ash Wednesday.
32: ;; Print as a Gregorian date.
33: (setq
34: f (fixed-from-gregorian (list t-year t-month t-day))
35: ti-year (first (islamic-from-fixed f)))
36: (dotimes (i 500)
37: (setq iy (+ (- ti-year 250) i)
38: r (fixed-from-islamic (list iy 9 1))
39: g-year (gregorian-year-from-fixed r)
40: aw (- (easter g-year) 46))
41: (if (equal aw r)
42: (format t "~a~%" (gregorian-date-string r))))
The -q in the shebang line tells CLISP not to put up its typical welcome banner. I had to write my own gregorian-date-string function (Lines 16–23) because calendrical.lisp doesn’t have one, but it was pretty easy.
In fact, it was all pretty easy. I haven’t programmed in Lisp or Scheme in quite a while, but I quickly remembered how fun it is. The only tricky bits were:
- learning how to handle the multiple value output of
get-decoded-time(Lines 26–28); - remembering how to handle more than one variable assignment in
setq; and - recognizing that what the ELisp calendar library calls “absolute” dates, the Common Lisp calendar library calls “fixed” dates.
R&D’s library has an easter function for getting the date of Easter for a given (Gregorian) year; Line 40 gets the date of the associated Ash Wednesday by going back 46 days from Easter.
The output of ramadan-lent was
Wednesday, February 6, 1799
Wednesday, February 24, 1830
Wednesday, February 22, 1928
Wednesday, February 18, 2026
Wednesday, March 7, 2057
Wednesday, February 16, 2124
Wednesday, March 5, 2155
Wednesday, February 13, 2222
Wednesday, March 2, 2253
The most common gap between successive correspondences was 98 years, but there were occasional gaps of 31 and 67 years.
It took only a few extra lines at the end to include a check for Chinese New Year. Here’s ramadan-lent-new-year:
1: #! /usr/bin/env clisp -q
2:
3: ;; The edited Calendrical Calculations code by Reingold and Dershowitz
4: (load "calendar.lisp")
5:
6: ;; Names
7: (setq
8: weekday-names
9: '("Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday")
10:
11: gregorian-month-names
12: '("January" "February" "March" "April" "May" "June"
13: "July" "August" "September" "October" "November" "December"))
14:
15: ;; Date string function
16: (defun gregorian-date-string (date)
17: (let ((g-date (gregorian-from-fixed date))
18: (weekday (day-of-week-from-fixed date)))
19: (format nil "~a, ~a ~d, ~d"
20: (nth weekday weekday-names)
21: (nth (1- (second g-date)) gregorian-month-names)
22: (third g-date)
23: (first g-date))))
24:
25: ;; Get today's (Gregorian) date.
26: (multiple-value-setq
27: (t-second t-minute t-hour t-day t-month t-year t-weekday t-dstp t-tz)
28: (get-decoded-time))
29:
30: ;; Loop through 500 Islamic years, from 250 years ago to 250 years in
31: ;; the future and find each Ramadan 1 that corresponds to Ash Wednesday
32: ;; and Chinese New Year.
33: ;; Print as a Gregorian date.
34: (setq
35: f (fixed-from-gregorian (list t-year t-month t-day))
36: ti-year (first (islamic-from-fixed f)))
37: (dotimes (i 500)
38: (setq iy (+ (- ti-year 250) i)
39: r (fixed-from-islamic (list iy 9 1))
40: g-year (gregorian-year-from-fixed r)
41: aw (- (easter g-year) 46))
42: (if (equal aw r)
43: (let ((ny (chinese-new-year-on-or-before r)))
44: (if (equal ny (1- r))
45: (format t "~a~%" (gregorian-date-string r))))))
The chinese-new-year-on-or-before function (Line 43), which is in the library to aid in the writing of the typically more useful chinese-from-fixed function, turned out to be just what I needed here. It gets me the fixed date of Chinese New Year that’s on or before Ramadan 1. I then check to see if that’s exactly one day before Ramadan 1 in Line 44.
This script’s output was
Wednesday, February 6, 1799
Wednesday, February 18, 2026
Wednesday, February 16, 2124
Wednesday, February 13, 2222
We see that last week’s triple correspondence hadn’t occurred in 227 years, and it’ll be another 98 years before the next one. Thanks to TJ for getting me to look into this rare event.
I’ve had a copy of the second edition of Calendrical Calculations (called the Millenium Edition because it came out in 2001) for over twenty years. As I was fiddling with ramadan-lent and ramadan-lent-new-year, I ordered the fourth (or Ultimate) edition so I’d have the best reference on the functions in calendar.lisp. You can expect more posts on calendars and astronomical events as I dig into it.
-
Not in SBCL, unfortunately. As best as I can tell, SBCL wants every function to be defined in terms of previously defined functions, and R&D didn’t write their code that way. CLISP is more forgiving in the order of definitions. ↩