What time is March 14, 2021 2:00 AM?
This post is just some fascinating (or quite boring depending on your perspective) minutia that arises in the handling of time and timezones in programming. This deals with a little corner that exists in almost every time library yet I’ve never had much reason to explore it. I had just assumed that all major programming environments would return a consistent answer to this question. Surprisingly, the answers are a bit less consistent than I expected.
What special about March 14, 2021 2:00?
That’s the exact time in 2021 for the vast majority of Americans when the dreaded Daylight Savings Time began. I’m on the east coast (my tz database timezone is America/New_York) and so, for me, this was the time that my timezone transitioned from Eastern Standard Time (5 hours behind GMT) to Eastern Daylight Time (4 hours behind GMT). Because of this transition, there is no 2am on March 14, 2021. If you were watching a microwave clock (assuming that it adjusted for DST) early in the morning on March 14, it would transition from 1:59AM to 3:00AM. In local time, there is no such thing as March 14, 2021 2:00AM. The diagram below is another illustration of why 2:00 never happens on March 14.
But what happens if I use the time library in my favorite programming language and try to construct that time? This adventure began for me in Go, which is a programming language I work in often. I was building test cases for a function that operated on time.Time values and, unsurprisingly, I wanted to include test inputs that verified that this function acted reasonably during daylight savings time transitions. So I typed the following code not really knowing what would be returned.
What it returns, when using Go 1.18.3, is 2021-03-14T01:00:00-05:00. That result was a little surprising to me, at first, but it does make sense. This clearly interprets the input as indicating 2am EDT (the time zone after the transition) but that time would be presented in EST (the time before the transition) for America/New_York and so the result is 1am EST. I took a look at the documentation for Go’s time package and, to my surprise, this case is covered in the documentation. As it turns out, not all time libraries bother to cover this behavior in documentation. Here's what Go promises in this case,
A daylight savings time transition skips or repeats times. For example, in the United States, March 13, 2011 2:15am never occurred, while November 6, 2011 1:15am occurred twice. In such cases, the choice of time zone, and therefore the time, is not well-defined. Date returns a time that is correct in one of the two zones involved in the transition, but it does not guarantee which.
So Go is light on its guarantees, you’ll get a time interpreted in one of the two time zones involved in the transition, you can’t expect anything more than that. Given Go’s reluctance to offer more predictable guarantees, I started exploring other languages and their associated time libraries. Would there be consistency here? Would the documentation describe the expected behavior of this case? I took a look at just a few of the most popular languages to see.
When tlocal represents local time repeating multiple times at a negative time zone transition (e.g. when the daylight saving time ends or the time zone offset is decreased due to a time zone rule change) or skipped local time at a positive time zone transitions (e.g. when the daylight saving time starts or the time zone offset is increased due to a time zone rule change), tlocal must be interpreted using the time zone offset before the transition.
Another very popular language that receives a lot of hate, what does it do?
For Overlaps, the general strategy is that if the local date-time falls in the middle of an Overlap, then the previous offset will be retained. If there is no previous offset, or the previous offset is invalid, then the earlier offset is used, typically "summer" time.. Two additional methods, withEarlierOffsetAtOverlap() and withLaterOffsetAtOverlap(), help manage the case of an overlap.
ZonedDateTime even exposes API to get a time using either the earlier or later offset.
2021-03-14T02:00:00-04:00 That’s surprising and definitely inconsistent with other languages. What I find strange about this result is that it's not even in the requested time zone. It seems to have interpreted the time in EDT (the timezone at the end of the transition) and comes up with the same UTC time as Go (6am UTC). However, that time in America/New_York should be in EST and not EDT. And what about documentation? Yes, but you have to work hard to find it. Python’s datetime library does not provided a tzinfo implementation that handles DST transitions so one has to look to the dateutil.tz module for that and also for the appropriate documentation. The behavior is described in the documentation for the resolve_imaginary function.
This function assumes that an imaginary datetime represents what the wall time would be in a zone had the offset transition not occurred, so it will always fall forward by the transition’s change in offset.
What does that mean? I don’t have a clue and I can’t explain Python’s result after having read it. One more …
Here's a summary of the results from each of the languages I tried. Python and Go stand out, of course, and Python stands out most of all since the result is not even in the requested time zone.
What about Nov 7, 2021 at 1am?
There is, of course, the other transition that happens each year when we transition from daylight savings time back to standard time. In this case, though, the time happens twice. If you were staring at your microwave at 1:59am on Nov 7, 2021 (assuming again that you have a fancy microwave that adjusts automatically), it would transition to 1:00am. An hour before that it would have also shown 1:00am. For the sake of brevity, I’ll just summarize. For this transition, all the languages come up with the same answer, strangely.
Fascinating to some, including me, but I’m sure boring to others. I did not, however, expect to find differences in popular programming languages. I should also add that I intended to include rust in this list, but rust’s chrono crate rejects ambiguous times, either by panicking or by returning the Option type of None. I quite like that approach, honestly. I guess the only real take-away here is to avoid these ambiguities that exist in the date time libraries of most programming languages. Otherise, you may end up having a bad time.