The Timezone-Specific, Browser-Specific DateTime Bug of 2014

timezone-bug

The start of the new year is always a great opportunity to reflect on time – looking back at the past year and looking forward to the next.

But here at DoneDone, we see it as an opportunity to think about timezones, and specifically the difficulties they can cause for developers.

A great example of this was sent to us by a DoneDone customer in late 2014. The user noticed that DoneDone’s datepicker popups were mysteriously rendering some September dates incorrectly. Upon further investigation, it turned out that September 27 was being displayed twice in each calendar.

September 27, 2014 appears twice

September 27, 2014 appears twice

After a bit more digging, we were able to assemble a few more clues:

  • The duplicate dates only appeared to render for the New Zealand timezone (NZDT/NZST).
  • The bug could only be replicated on Safari 6.2 running on OSX 10.8.5.
  • The date offset was corrected when switching the calendar to the next month.

Our first hunch was that the bug was related to Daylight Saving Time, and a quick Google search confirmed that DST began on September 28, 2014 in New Zealand, very close to our problem date.

Next, we cracked open the JavaScript function responsible for building the calendar elements, and located the loop which iterated through the days in each month:

// (date variable is initially set to the date value of the first cell displayed in the calendar)
i = 0;
while (i < 42) {
  // (additional code removed for brevity)
  i++;
  date.addDays(1);
}

Date.prototype.addDays = function (n) {
  this.setDate(this.getDate() + n);
};

This code block simply begins with the first day displayed in the current calendar view and increments it 42 times (the calendar will show a maximum of 6 rows, each with 7 days). At the end of the loop, the counter is incremented and a full day is added to the date variable.

After adding a bit of debugging code, we were able to track how the date changed in multiple browsers between September 27 and September 29.

// Safari 6.2, OSX 10.8.5, New Zealand timezone
2014-9-27 00:00
2014-9-27 23:00
2014-9-28 23:00
2014-9-29 23:00

// Other browser/timezone combinations
2014-9-27 00:00
2014-9-28 00:00
2014-9-29 00:00

Now we’re getting somewhere! The debug output confirmed that Safari’s Daylight Saving Time calculations were a bit off – adding a day to September 27 at midnight resulted in the “next” day being September 27 at 11:00PM.

At this point, we considered a variety of options to correct this bug. We could rework our addDays() function to split the date into integers for year/month/day and then write new logic to increment the day value and rebuild the date object. Or, we could rewrite our datepicker to use a more robust library like Moment.js.

In the end, we decided the simplest option would be to avoid using midnight when incrementing days. This update did the trick:

// (date variable is initially set to the date value of the first cell displayed in the calendar)
i = 0;
while (i < 42) {
  // (additional code removed for brevity)
  i++;
  date.setHours(12, 0, 0, 0);
  date.addDays(1);
}

By moving the clock to noon, we avoid any potential calculation problems that occur at the beginning or end of the day. And since the time portions of each day are discarded after each calendar table cell is rendered, we avoid the risk of inadvertently changing the value of any dates sent to or from our calendar controls. Here’s the debug output with the fix applied:

// Safari 6.2, OSX 10.8.5, New Zealand timezone
2014-9-27 12:00
2014-9-28 12:00
2014-9-29 12:00

// Other browser/timezone combinations
2014-9-27 12:00
2014-9-28 12:00
2014-9-29 12:00

This bug began as a real head-scratcher, but was a great exercise in QA. A user found a bug and reported it, and we worked together to establish steps for reproducing the issue. Our development team then found a few logical points in the DoneDone codebase for further testing, which shed more light on the root cause of the issue. Finally, we considered and tried a few fixes, and ultimately settled on one that resolved the bug without a huge impact on the rest of our code.

It’s important to note that investigating this bug did make it clear that a more robust solution may be more desirable in the future. By switching to a mature library like Moment.js, we can avoid these types of edge cases in the future. After all, that’s the same conclusion most developers arrive at when this type of problem pops up:

Jeremy Kratz is a developer at DoneDone. Follow him on Twitter via @jwkratz.