On 31st December 2009 a unit test that had been running successfully in the Caplin continuous integration environment for two months suddenly started failing.
The test in question created a JavaScript Date object, modified it using various setter methods, then verified that the format method returned the expected value. The code for this is shown in the snippet below.
var oDate = null; Test.setUp = function() { oDate = new Date(); oDate.setYear(2012); oDate.setMonth(9 - 1); // September oDate.setDate(8); oDate.setHours(17); oDate.setMinutes(05); oDate.setSeconds(19); oDate.setMilliseconds(55); }; Test.format_Ext = function() { assertEquals("2012-09-08 17:05:19", oDate.format("Y-m-d H:i:s")); };
This code may not be to be everyone’s liking, since it seems a verbose way of setting a specific date (it’s mutating an object when it could have constructed it with the correct arguments to start with, and not everyone likes setters). However it seemed surprising that it was suddenly failing. After all this test had been passing for two months, but it was now reliably failing in IE, Firefox and Chrome.
For the record, the failure was caused because the expected value was “2012-09-08 17:05:19″, however the actual value was “2012-10-08 17:05:19″.
It took a little while to get to the bottom of this issue, which was not entirely unsurprising given its cause. When a Date object is constructed with no arguments it represents the date and time at the point it was created. In addition to this, the Date object always wants to represent a valid date. The issue occurs when attempting to set the month to September when the day is the 31st. Since September doesn’t have 31 days this is automatically rolled over to 1st October for us, as illustrated in this commented code snippet below:
Test.setUp = function() { oDate = new Date(); // year = 2009, month = Dec, day = 31 oDate.setYear(2012); // year = 2012, month = Dec, day = 31 oDate.setMonth(9 - 1); // year = 2012, month = Oct, day = 1 // 31st Sep is invalid so it rolls over // to 1st Oct! oDate.setDate(8); // year = 2012, month = Oct, day = 8 // other setters... };
Looking at the documentation for the Date object it is easy to eliminate this issue by modifying the code to change the year, month and day atomically. Several potential options are shown below:
// my preferred solution - construct the Date object with // the correct year/month/day to start with oDate = new Date(2012, 9 - 1, 8); // or oDate = new Date(); oDate.setYear(2012, 9 - 1, 8); // or oDate = new Date(); oDate.setYear(2012); oDate.setMonth(9 - 1, 8);
Regardless of which solution you believe is best, the most important lesson that I took away from this is how critical it is to name your methods appropriately. It turns out that the documentation for Date object from both MDC and MSDN provide clear explanations about the behaviour of the method, however they are undermined by the benign nature of its name – most developers have used setter methods ad infinitum and wouldn’t bother to read the documentation.
In our case, the call to setMonth() was made in the good faith that it would set the month to the expected value. It was not anticipated that it would set the day too, which in turn would increment the month to a different value from the one that was set. In fact if the setDate() method had been called the line above setMonth() then this issue would not have occurred, which is contrary to what I expect. I struggle to think of any reason why setters should not be commutative.
This article is not intended to be a criticism of the setMonth() method behaviour, instead it is a reminder to all developers of the importance of method names. Method names should describe what they do since the users of the method may make assumptions about what it will do based on that name without reading any documentation associated with it. Robert C. Martin discusses this in much better detail in the chapter “Meaningful Names” in his excellent book Clean Code: A Handbook of Agile Software Craftsmanship which I would thoroughly recommend to all developers.
Thank you so much for posting this — as it happened I was testing a September date out today, August, 31st, and ran into into. Was pulling my hair out until I found this.
Hi Chris,
I can feel your frustration with this, I recall only too well the confusion I felt when I was looking at this issue.
After writing this post, I have shown several developers the original example code and asked them to identify why it is exhibiting unexpected behaviour. No one has managed to answer the reason why, although a few did suggest constructing the Date with the correct parameters in the first place, which would have avoided the issue.
I’m glad that the post helped you solve your issue.
Thanks,
Ian