Properly calculating time differences in JavaScript

Let me tell you a tale about a fat-client application that has nice some time-related logic written in JavaScript. We want to calculate the difference between two dates, measured in days. Easy, you say, just use the Date object and do some calculating.

As a JavaScript veteran you know that you have to use new Date() instead of Date() because the second one returns a string for some reason, you recall that the month of October is identified by the number 9 because we start counting the months starting at 0 and quickly figure out that subtracting two Date objects results in a number which is the amount of milliseconds passed between two moments.

var DAY_IN_MS = 24 * 60 * 60 * 1000;
var d1 = new Date(2012, 9, 27);
var d2 = new Date(2012, 9, 28);

console.log((d2 - d1) / DAY_IN_MS); // yields 1

Looks fine, doesn’t it? So just wrap it in a function, unit-test it and be done with it? Not so fast there. Let’s just change the dates ever so slightly

var DAY_IN_MS = 24 * 60 * 60 * 1000;
var d1 = new Date(2012, 9, 27);
var d2 = new Date(2012, 9, 28);
var d3 = new Date(2012, 9, 29);

console.log((d2 - d1) / DAY_IN_MS); // yields 1
console.log((d3 - d2) / DAY_IN_MS); // yields 1.0416666666666667

This is the point where most developers start cursing. Is this a new way in which JavaScript is broken? It isn’t, because the number is completely accurate.

The JavaScript object created by new Date(2012, 9, 28) represents midnight on the 28th of October, 2012 in your local time zonenew Date(2012, 9, 29) represents midnight the following day.

Subtracting the first from the seconds yields the number of milliseconds that have passed between those two moments, which, as you probably have guessed, includes the extra hour put in because of daylight savings time.

> new Date(2012, 9, 29);
Mon Oct 29 2012 00:00:00 GMT+0100 (CET)
> new Date(2012, 9, 28);
Sun Oct 28 2012 00:00:00 GMT+0200 (CEST)
> (new Date(2012, 9, 29) - new Date(2012, 9, 28)) / 60 / 60 / 100
25

So where is the error? The error is in our assumption that a day has 24 hours, because depending on how you define a day, it hasn’t – October 28th 2012 has 25 hours.

If you Google « JavaScript time difference », most people just use Math.round (1) or simply use flat-out buggy code (1 2) and call it a day (pun intended), but that is not how we roll here.

What do we really mean when we ask « How many days have passed between two dates in the calendar »? We usually mean « How many midnights have happened between these two dates? ». Unfortunately, because of DST, you can’t just use the number of milliseconds between two dates at midnight to calculate how many midnights have happened, because some of them are more or less than 24 hours apart. If only there was a magical place that doesn’t have this madness going on…

Luckily, there is, and that place is UTC. UTC is a time measuring system that does not have daylight savings time.

Edit: as pointed out in the comments, the rabbit hole goes down even further – officially, even in UTC, a day might have more than 24 hours because of leap seconds. Fortunately for us, the ECMA-262 specification explicitly ignores leap seconds and we can go about our business. If JavaScript would implement UTC correctly, we would have to account for leap seconds or useUT1.

The JavaScript Date API is just as beautiful as most other JavaScript APIs: While the only useful use of the Date object is by using it as a constructor (with new), the way to use UTC is by using the function Date.UTC which returns a unix timestamp. This is the JavaScript time API in a nutshell:

> new Date(2012, 9, 29);
Mon Oct 29 2012 00:00:00 GMT+0100 (CET) // (a somewhat useful object)

> Date(2012, 9, 29);
'Mon Nov 05 2012 16:18:12 GMT+0100 (CET)' // (a string - no relation to the parameters)

> Date.UTC(2012, 9, 29);
1351468800000 // (unix time in milliseconds)

> new Date.UTC(2012, 9, 29); // failure
TypeError: function UTC() { [native code] } is not a constructor
    at repl:1:9
    [....]

The correct calculation, without using Math.round or other hacks therefore is

var DAY_IN_MS = 24 * 60 * 60 * 1000;
var d1 = Date.UTC(2012, 9, 27);
var d2 = Date.UTC(2012, 9, 28);
var d3 = Date.UTC(2012, 9, 29);

console.log((d2 - d1) / DAY_IN_MS); // yields 1
console.log((d3 - d2) / DAY_IN_MS); // yields 1

These kinds of bugs are sneaky because they only show up for certain input values (I wonder if I would have noticed it if I hadn’t tested the code last week around the DST change) and usually don’t show up in unit tests unless you happen to know what you are looking for. The results are often nearly correct, and we are not used to thinking about time zones and we often hold invalid assumptions about time. Always using UTC isn’t a solution either, because sometimes we want the local time zone to be considered.

Libraries like Moment.js help, but the real protection against these kinds of bugs is to know about time zones, time measurement system and thinking about what you are actually calculating instead of simply throwing a Math.round in there to make it all work.

Just as anybody that has had the pleasure of seeing Rent will tell you: while a year has five hundred twenty-five thousand six hundred minutes, it still is difficult to measure the time of the year.

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s