Leap Years, Lost Days, and Coding Calendars: A JavaScript Adventure

• ~900 words • 4 minute read

Dates are hard and JavaScript is odd. Two universal truths.

In belated honor of 2024 being a leap year, here's a story about how I learned 2011 was not a leap year, ISO 8601, proleptic Gregorian calendars and forking Node.

To belatedly commemorate the leap year of 2024, here is a story about how I discovered of 2011 wasn't a leap year, dove into the intricacies of ISO 8601, the concept of proleptic Gregorian calendars, and decided to fork Node on a whim.


Understanding the Proleptic Gregorian Calendar

JavaScript implements (best as I can tell) the ISO 8601 standard to avoid ambiguous date/time expressions. This includes an adherence to something called the proleptic Gregorian calendar.

The Gregorian calendar is more or less the calendar system we all use today and was established on Friday, October 15th, 1582. The curious thing about this is that it came immediately after Thursday, October 4th. They had to do this to synchronize with the switch from the Julian calendar. As are a result, those ten days never happened.

The proleptic Gregorian calendar attempts to make this less confusing by basically pretending the Gregorian calendar has always been in use. In this world, when looking back at dates that happened prior to the switch, a date like October 7th, 1582 can exist. This is a modern convention to help with historical consistency.

JavaScript is happy help here:

new Date("October 11 1582")
// 1582-10-11T04:56:02.000Z

Quirks of JavaScript Date Handling

However, JavaScript has some quirks. It does care about some days that make no sense and could never happen:

new Date("October 33 1979")
// Invalid Date

But it doesn't care about all the days that could never happen:

new Date("February 31 2024")
// 2024-03-02T05:00:00.000Z

It doesn't throw an Invalid Date even thought February 31 could never possibly exist. It's actually returning March 2nd, 2024 in this example!

If you dive in the source you can see it takes a (somewhat lazy) assumption by checking the days first and assuming 1 to 31 can always be valid. It then checks the month to see if it has that many days. Instead of throwing an error if it doesn't, it just substracts the max number of days from that month and uses the remainder to figure out which day it is in the next month.

11ty and leap years

Something funny happened when I resurrected some old blog posts from 2011. The tool I use to build this site (11ty) started throwing errors. I assumed there was some weird piece of misformatted metadata in one of the posts, but what I found was a lot stranger.

Invalid Date appearing in the RSS feed

The offending post was written on February 29th, 2011. It turns out that day never existed—it's only a leap year if it is divisible by 4, except for end-of-century years which must be divisible by 400.

Me asking Google if 2011 was a leap year because I am too lazy to do math

Finally finding the culprit—a file from 2011 on a day that did not exist

While JavaScript itself doesn't care, some package in the 11ty stack trying to parse and understand dates must care. I'm curious which one it is, though I never went looking.

In a nut-shell, because I'd written this date down wrong over a decade ago, and because something in 11ty is a stickler for real dates, I had to change the date to fix my blog. The funny part is I had no way of knowing, 10+ years later, if I'd actually written it on March 1st or February 28th.

Apple, NSCalendar and sticklers for reality

It bothers me (irrationally) that JavaScript would try to coerce February 31, 2024 into a real date but lazily stand-buy and be fine with October 7th, 1582. I am reminded once again that software—like all human constructs—really just boils down to a mishmash of people's opinions in the moment.

It makes sense to me that the company whose founder gave so much thought about typography in early computing would take the time to put thought into their standard calendar class to account for the Julian to Gregorian transition in a more nuanced and historically-accurate way.

In NSCalendar, similar to how JavaScript will move February 31st into the next month, any days that fall between October 4th and 15th in 1582 will automatically get moved forward 10 days.

One of the more fun macOS easter eggs live sin the Calendar app. If you go back to October 1582 you can see Thursday, October 4th immediately followed by Friday, October 15th.

macOS Calendar app showing October 1582 and how the 10 days are missing

Let's fork Node!

I like the way NSCalendar handles this situation better. So I did what any sensible person would do: I dusted off my C++ brain and forked Node.

I'd never built Node from scratch, let alone fork an entire language just to make such a silly, opinionated tweak to a standard model. The documentation is clear though, and the project is soundly structured. After some hunting, pecking and reading I found the place to introduce my change:

  • https://github.com/georgemandis/node/blob/0663467e521627523f3dae908bf9eafe02377beb/deps/v8/src/date/date.cc#L96

Voila.

My forked version of Node with different opinions around date handling in 1582

If you've found any of this nonsense helpful or intriguing—and particularly if you have your own silly explorations to share—please feel free to reach out! I love a good goofy deep-dive into the inane.