Your app displays a wrong birthdate

Lately a user was complaining that he sees a wrong birthdate in our app. I asked our business guy to verify if it differs by one day just to ensure that we don’t have a timezone bug here. Actually this wasn’t the problem but my business guy went mad. “How can it happen that we’re even talking about bugs like these?”

Of course he’s absolutely right but date and time problems are not as simple as they might seem to be: Imagine you were born on 9 June 2006 18:00 in Munich (that’s 6pm if you are using a 12-hour clock). The timezone Australia/Sydney is 10 hours ahead of Munich so at the same moment in time the clock displayed 10 June 2006 04:00 in Sydney. This doesn’t affect your birthdate. It will always remain 9 June no matter where you are. It did affect the opening game of the FIFA world cup which took place at the same time in Munich. Same moment in time, same places but enthusiastic football fans in Sydney went up early and watched the game “one day after” your birthday.

What if you would have made holidays in Australia during the world cup? As football fan you entered all the important dates in your calendar. Most probably your calendar added your local timezone to these events and will display them converted to the local time during your holidays. Great, you won’t miss any match! But what about the date of your return flight? Hopefully you either added the timezone of Sydney or entered the converted time (not the time on the ticket!) to your calendar. If not you’ll miss your flight.

I don’t want to bore you too much with more examples but I think that many date bugs are not technical issues in the first place. Quite often people just did not care about timezones.

If you feel certain how to handle dates from a business point of view how can you solve that technically? You mainly have three choices:

Timestamps

A timestamp counts the seconds or milliseconds relatively to a certain moment in time at a certain place. In computer science we commonly use the epoch time which started at 1 January 1970 00:00:00 at the prime meridian.

Markings of the prime meridian at the Royal Observatory, Greenwich.

The timestamp 1149868800000 marked the beginning of the football match mentioned above. With this information it’s easy to calculate the time at Greenwich or convert it to any other timezone. The programming language of your choice will most probably offer you some kind of Date object or function based on timestamps. The thing you always need to keep in mind is that you need a timezone if you want to convert a timestamp to a human readable date. If you don’t specify one there will be a default which is either UTC or the timezone where your code is executed. It may be obvious that a Date based on a timestamp is not the best way to store a birthdate but you might start using it when you want to calculate the age.

Local Dates

A local date represents a date (and time) without a timezone as a string. Examples using ISO-8601 are 2016-06-09or 2006-06-09T18:00:00. They are used if you want to store birthdays or historical dates like 1945-05-08 (end of World War II). You usually don’t want to convert them to other timezones. Combined with a time you can describe that something should happen at a certain time without specifying where. For instance you might want to toast to the new year at 2019-01-01T00:00:00. People in Munich and Sydney will use the same local date and time but Sydney will do it 10 hours before Munich.

Zoned Dates

Zoned dates are also using a string representation for date and time but they are adding a timezone: 2006-06-09T18:00:00+02:00 or 2006-06-09T16:00:00Z. The +02:00 specifies the timezone of Munich (One hour plus for one timezone eastern of UTC plus one hour daylight saving time). The Z is equivalent to +00:00. Wondering why hours and minutes are used for timezones? Full hour time deviations are the rule but there are also timezones using 30 or 45 minutes deviation. Zoned dates have the advantage that you mark a certain moment in time combined with the information where it happened. The reader has the chance to either convert this time to another timezone or use the given one.

So now the business and technical part are clear and nothing can go wrong, right? No, one more problem is: handling dates is not under your control. Your app most probably receives data from an API which might connect to another backend system which might store it in a database (or connect to another backend system …). The chances are high that one of the API developers was using a timestamp where you would have chosen a local date (or the other way round). Luckily framework X is doing automatic conversion but it’s calculating with UTC while you were expecting it’s using your timezone.

And even if everything is handled on your own it’s easy to mess things up. Examples? Let’s do some JavaScript:

> new Date('2006-06-09')
2006-06-09T00:00:00.000Z
> new Date(2006, 5, 9)
2006-06-08T22:00:00.000Z

We created two date objects representing 9 June 2006. Don’t get confused by the 5 in the second example. JavaScript is counting months 0-based. That’s obvious.
The interesting thing here is that the second date represents 8 June 2006. Why’s that? The Date constructor assumes local time when it’s called with more then one arguments, otherwise UTC is used. The second date is two hours behind because currently I’m +02:00
I’m using node and the output above is the result of toISOString(). It may be clearer when using toString().

> new Date('2006-06-09').toString()
'Fri Jun 09 2006 02:00:00 GMT+0200 (CEST)'
> new Date(2006, 5, 9).toString()
'Fri Jun 09 2006 00:00:00 GMT+0200 (CEST)'

The result is the other way round. So always think about how a date was constructed before deciding if you need a method with UTC or ISO in the name or not.

So if the date constructor assumes UTC when using a date string what happens if you pass a different timezone?

> new Date('2006-06-09T18:00:00.000+02:00')
2006–06–09T16:00:00.000Z
> new Date('2006-06-09T18:00:00.000+02:00').getHours()
18
> new Date('2006-06-09T18:00:00.000+02:00').getUTCHours()
16

The timezone is used. So you should really have a careful look if your API uses Local or Zoned Dates before constructing a date. 
If you think you can get around this by using Date.parse please stop:

It is not recommended to use Date.parse as until ES5, parsing of strings was entirely implementation dependent.

Ok, but most of the time you just want to reformat a date according to the users locale and these days we have Intl.DateTimeFormat! That’s right but be aware that you’ll have to construct a Date object to use it:

> new Intl.DateTimeFormat('de-DE').format(new Date('2006-06-09'))
'8.6.2006'

What’s wrong? You constructed a UTC Date but Intl.DateTimeFormat will use the local timezone when formatting it. Your user is currently in a timezone west of UTC and you changed his birthdate. You can try to circumvent this by forcing a timezone:

> new Intl.DateTimeFormat('de-DE', { timeZone: 'Europe/London' }).format(new Date('2006-06-09'))
'9.6.2006'

This will work for displaying a birthdate but be careful with user input. You should not display dates with a specific timezone without calculating with this timezone when parsing user input. Otherwise your datepicker might “jump” one day if your user is in another timezone.

… there must be a lib for that

I’m not a big fan of using libraries where possible but if you’re stuck in date/time hell Moment.js might be worth a try. In contrast to the JavaScript Date Moment is simply consistent:

With Moment, the date is always interpreted as local time, unless you specify otherwise.

This doesn’t fix all problems but it makes fixing them much easier. Moment also provides a timezone extension which helps when you need to convert dates from one timezone to another. You can also set a timezone explicitly in your unit test and check results in different timezones.