A glitch in February of the year 0

lukasgelbmann1 pts0 comments

A glitch in February of the year 0

Blog > PostA glitch in February of the year 0<br>by Lukas Gelbmann, 2026-06-26A technical report about a rare correctness issue that we found and fixed.Recently, as we were adding support for timestamps in the distant past,<br>a team member noticed during testing that some timestamps weren’t<br>handled correctly. The issue could be easily reproduced with the<br>timestamp 0000-02-03 04:00 Europe/Oslo.

A first investigation showed that the problem affected all time zones,<br>but only in February of the year 0 (as well as the last few days of<br>January).

Most time series don’t have timestamps that are two thousand years in<br>the past. But, of course, we want to parse all timestamps in the<br>supported range correctly, even the rare cases dating back to antiquity.

Time for bug hunting

We started looking for the bug, assuming that, surely, it would be in<br>our own code. We use the calendar logic provided by the PHP runtime (the<br>DateTimeImmutable class), but we still have some non-trivial<br>processing on timestamps. To deal with timestamps that are ambiguous<br>because of a time zone transition, we compute to a Unix time stamp<br>internally, and then convert it to a PHP DateTimeImmutable.

The year 0 is a bit of an outlier in two ways. First, it doesn’t exist<br>in the traditional Julian calendar that historians use (they would call<br>it 1 BC instead). Second, in the proleptic Gregorian calendar with<br>astronomical year numbering (which is the calendar that we use on<br>28times), it’s a century leap<br>year. Century leap<br>years are the exception to an exception: years divisible by 100 are not<br>leap years, unless – like the year 0 – they are divisible by 400.

This gave us a vague idea of why the year 0 might be impacted by a bug.<br>But since other century leap years (such as 2000) were unaffected, this<br>couldn’t be a full explanation.

So we went through our code step by step and found the problem. To our<br>surprise, it wasn’t in our code. The problem was related to the idiom we<br>used to convert a Unix time stamp to a DateTimeImmutable object.

The root cause

Below are three ways of converting a Unix time stamp to a<br>DateTimeImmutable in PHP.

\DateTimeImmutable::createFromFormat('U', '-62164356180')

(new \DateTimeImmutable('@0'))->setTimestamp(-62164356180)

new \DateTimeImmutable('@-62164356180') // incorrect return value

These three should be completely equivalent – and for most timestamps,<br>they are. But unlike the first two, the last variant gives a result<br>that’s off by one day for February of the year 0. At time of writing,<br>this happens in all recent PHP releases. As luck would have it, the last<br>method is also the one we used in our code.

(You can also substitute DateTime for DateTimeImmutable in these<br>three snippets. DateTime has the same problem with the last variant.)

Fixing the issue

For our own purposes, the fix was simply to use one of the first two<br>methods. This is also what I would recommend to other PHP programmers<br>using DateTime or DateTimeImmutable.

I also opened a pull<br>request to fix the bug in<br>the library timelib, which provides date/time functionality. PHP’s<br>DateTimeImmutable uses this library internally.

The problem ended up being that timelib has two implementations for<br>converting a Unix timestamp to a date in the proleptic Gregorian<br>calendar. One of them has a range check that uses the wrong date – a<br>date that falls in January of the year 0, around a month before the<br>century leap day instead of after it. This causes all results before the<br>century leap day to be off by one day. I proposed fixing it by making<br>all callers use the correct algorithm.

This issue will hopefully be fixed in upcoming releases of timelib and<br>PHP. It certainly made for a satisfying bug fix – we have a clean<br>workaround, and improving timelib and PHP is a nice bonus.

year datetimeimmutable time timestamps leap february

Related Articles