Tricky, tricky dates
Any programmer who worked with dates would tell that dates are a complex subject. Time zones, UTC, daylight saving time, leap years, and, my favorite of all—leap seconds that “are not entirely predictable”—can make your life miserable very quickly.
And it did.
I was working on a feature of a project I inherited from a group of developers, when I noticed that the dates weren't behaving very nicely. The interface had a form, and in this form, there was a field specifying a time: hour, minute, and second. For example: “09:37:20.” When the form was submitted, this value shifted magically approximately nine and a half minutes back in time. Checking the code, I discovered a few interesting things.
Date initialization
The date was initialized like this: new Date(0, 0, 0, hour, minute, second, 0)
. It may take a moment to notice that there is an error here. And another moment to understand that what it looks like is not at all what happens under the hood. Let's go to the Mozilla's documentation page of Date
constructor to see what the different arguments mean here.
The documentation starts with a fun fact:
Any missing fields are given the lowest possible value (
1
forday
and0
for every other component).
So actually, the minimum value for day
is 1, but JavaScript doesn't complain if one specifies a lower value. Indeed:
$ new Date(2021, 5, 1, 14, 30, 45, 0)
2021-06-01T12:30:45.000Z
gives the 1st of June (in JavaScript, it's 5th month in a range starting from zero, so June, not May), while:
$ new Date(2021, 5, 0, 14, 30, 45, 0)
2021-05-31T12:30:45.000Z
moves us one day in the past, that is, the 31th of May. Actually, JavaScript doesn't even complain if you specify a negative value, so new Date(2021, 5, -9, 14, 30, 45, 0)
leads to the 22th of May, and new Date(2021, -5, -9, 14, 30, 45, 0)
is the 22th of July, 2020. Nor does it argue with you when you want it to create 32th of June, which would be convenient in a moment.
Year is a different beast. According to the same documentation:
Values from
0
to99
map to the years 1900 to 1999. All other values are the actual year.
In other words, the date of death of Caesar Nerva Trajanus (8 August 117) can be represented with a Date
constructor in JavaScript, but his birthdate (18 September 53) cannot. Not that you have to deal with those sorts of dates in JavaScript on daily basis.
The specificity of the treatment of the year argument, and the fact that zero is lower than the minimum accepted value for the day argument means that 0, 0, 0
leads to the 31th of December of the year 1899. Since the application manipulates only the time, is it really that important whether the date happens to be in 2021 or a century ago? It appears that it is important, as soon as you consider the time zones.
Time zones
Most manipulations with the dates involve a sometimes implicit conversion from a time zone to another. The sole way to call a constructor builds a local time. You may have noticed that in the first example in this article, Node.js gave an UTC representation of the date I created. 14:30 GMT+2 was translated into 12:30 UTC. Depending on the day and month, on my machine Node.js will be using either the Central European Standard Time (GMT+1) or Central European Summer Time (GMT+2). For instance, new Date(2021, 1, 26, 14, 30, 45, 0)
gives ...13:30:45.000Z
which means that it's GMT+1 which was used in February 2021 in my time zone.
Let's list the days when the time zone changed:
var prev = undefined;
for (var i = 1; i < 45000; ++i) {
var d = new Date(0, 0, i, 12, 0, 0, 0);
var offset = d.getTimezoneOffset();
if (offset != prev) {
prev = offset;
console.log(d.toString().substr(0, 15), Math.abs(offset));
}
}
Here's the result:
Steady pattern of two changes per year between 1916 and the Second World War, then between the oil crisis of 1970 and today. Maybe not the easiest thing to follow, especially around the change from GMT+0/GMT+1 to GMT+1/GMT+2 1 during the Second World War, but straightforward anyway. Things get very different at the very beginning of the century. Before 1911, instead of a usual shift expressed in multiples of sixty minutes (thirty in some countries), we have here a time zone with a nine minutes difference. 2
To see it in action, let's compare two dates:
$ var t1 = new Date(1911, 2, 11, 0, 0, 0);
$ t1.toUTCString();
"Sat, 11 Mar 1911 00:00:00 GMT"
$ var t2 = new Date(1911, 2, 11, 0, 0, -1);
$ t2.toUTCString();
"Fri, 10 Mar 1911 23:50:38 GMT"
What about the milliseconds BUE?
$ t1.getTime();
-1855958400000
$ t2.getTime();
-1855958962000
$ (t1.getTime() - t2.getTime()) / 1000;
562
Yes, that's a 562 seconds difference in one second. Δ𝑡=9:21. A French page of Wikipedia on this subject explains what happened. On the 9th of March 1911, it was decided in France to move to the official GMT system, which had a difference of 9 minutes and 20.921 seconds. And so they did, and now there is a bug in my app.
The bug itself
The application was doing two things: converting the string representation of the time to the Date
object, and then creating a string representation of it. By mistake, the string representation was using UTC, while the date itself, when created with the constructor of the Date
class, uses the local time zone. The original date such as new Date(0, 0, 0, 14, 30, 45, 0)
would become Sun Dec 31 1899 14:30:45 GMT+0009
, and when this date object was then converted to UTC, the time part would become 14:21:24
. Very conveniently, the hour remains the same, but the minutes and seconds are different from the original time.
Funny thing: the original developers have possibly tested their code, and noticed no errors at all. The project was originally developed in the United Kingdom, and with GMT, this weird piece of code works perfectly well. The new Date(0, 0, 0, 14, 30, 45, 0)
results in 1899-12-31T14:30:45.000Z
, and once converted to UTC, gives 14:30:45
—the same time as the original input.
Ideally, one would test code using different time zones. Just like internationalized applications are tested using different cultures—well, at least in the rare cases where they are actually tested. Node.js allows to change the time zone by setting the value of process.env.TZ
. In the current case, the exact statement would be: process.env.TZ = 'Europe/Paris'
.
On the other hand, you can't set the time zone in the browser. With a mature test infrastructure, it should be possible to set the local time zone of a given agent running the tests. Most projects, however, don't have the infrastructure as mature.
Another issue is to know what to test. I learned about the events of 1911 because I live in France and because I noticed the bug on my machine. I have no idea what fancy rules were applied a century ago in Indonesia, Armenia, or Paraguay. Neither do I expect developers from the UK or any other country to know what happened in France on the 9th of March.
The fact is, dates are complex. And JavaScript, by not sanitizing the inputs correctly, is not very helpful. Using mature libraries, such as Moment.js, which hide quite well the idiosyncrasies of JavaScript while allowing to set the time zone globally, is an option. With or without such libraries, one should always keep in mind that anything related to dates is as tricky as Unicode.