Tuesday 23 November 2010

Detecting Month-Ends in JavaScript

Background

I came across the following problem many years ago whilst doing some work for a financial services company. They had some code (which I don't think was written by me, but my memory is a little hazy on the subject) which attempted to establish whether the current date was the last date in a month. The code was written in JavaScript.

The normal way of performing such a test is to:

  • (a) establish what month the current date falls within,
  • (b) add a single day to the current date,
  • (c) establish what month the date resulting from (b) falls within, and
  • (d) compare values (a) and (c) - if they are different then the current date is indeed the last date in a month.

Steps (a) and (c) are easy - JavaScripts's Date object provides a getMonth() method which returns the zero-based month number (i.e. 0=January, 1=February, etc).

Step (b) isn't so easy as JavaScript's Data object does not provide any date arithmetic operations per se. That it, it does not provide an addDays() method or any equivalent thereof.

Well, the value actually stored internally by the Data object is the number of milliseconds between the date/time being represented and midnight (GMT) on 1st January 1970. And we can both read this value via the getTime() method, and write this value via the single-parametered constructor respectively. That is, I can obtain the internally stored value of a Date object called now via ms = now.getTime(); and I can create a Date object from this value via then = new Date(ms);. As we know how many milliseconds there are in a day (1000*60*60*24), we can add a day's worth of milliseconds to a given date to add one day to it - which is actually what step (b) requires.

So, putting it all together we get:

  var dateToday = new Date();
  var dateTomorrow = new Date(dateToday.getTime() + (1000 * 60 * 60 * 24));

  if (dateToday.getMonth() == dateTomorrow.getMonth())
    alert("NOT last day of month");
  else
    alert("IS last day of month");

So, has anyone spotted the floor in this logic? What if I told you the logic would fail (in the UK) on the last day of October 2004, 2010 and 2021? Yes - you've guess it - it'd to do with British Summer Time (BST) ending and the UK reverting to Grenwich Mean Time (GMT). At 2:00am on the last Sunday in October the clocks are put back to 1:00am. On such days there are therefore 25 hours (midnight to midnight). If the last Sunday in October happens to be the last day of the month (i.e. in 2004, 2010 and 2021) then the above logic will claim that it's not the last day of the month.

Such a bug can clearly lie dormant for many, many years before it's encountered.

The Solution

The solution is easy. The code shouldn't have used getMonth() to retrieve the current month, as that retrieves the month according to local time. To ignore the effect of the clocks changing we want to use getUTCMonth() instead. Problem solved. Or is it?

Remember that the Date object stores a GMT-relative value. So when it retrieves the date from the current system clock (as in date = new Date()) or has its value set via constructor arguments (as in date = new Date(2010, 9, 31)) it will always convert that to GMT before storing it. So if the current date/time happened to be midnight (00:00) on 31 October 2010 - which is still within British Summer Time - what's actually stored is 23:00 on 30 October 2010 - because that is the same date/time expressed in terms of GMT. So when we add 24 hours to this date we get 23:00 on 31 October 2010. As the month still has't changed, even a version of the above code which uses getUTCMonth() will report that 31 October 2010 is not the last day of the month as long as the time component is less that 01:00.

So a getUTCMonth-version of the above code will work, except for the one hour between 00:00 and 01:00 on the 31st October if that date is a Sunday. So for the fifteen years between 1st January 2005 and 31st December 2020 there's only a single hour during which it will fail. That asmuses me. It's the sort of really rare bug that would almost certainly go unnoticed during testing but could well cause problems (depending upon the end-of-month processing the test was intended to trigger).

An Actual Solution

There are probably lots of solutions involving the use of GMT (rather than local) time throughout. But the simplest approach is to realise that we really don't care about the time, but it's the time which is getting us into trouble. So let's change it.

Simply adding the following line into the original code, immediately after dateToday is initialised fixes things nicely:

  dateToday.setHours(12, 0, 0, 0);

Although this might seem a little like cheating, it works well. And that's good enough for me.

No comments:

Post a Comment