Recently, Microsoft Azure suffered a service outage that was related to the problem with date calculations when a leap year is involved. As developers, I’m sure many in our ranks have wondered how such an issue could have occurred. Instead of piling on, we should look at this as a teachable moment because a closer look reveals that this is not a trivial matter. I for one would like to thank Microsoft for its transparency in the posted explanation.
Some have argued “Why not use the DateTime AddYears() method? According to the post, MS, in it’s PaaS (Platform as a Service) functionality, runs VM’s through something called a Guest Agent (GA) and that such GA creates a transfer certificate that is valid for one (1) year. It would seem reasonable at first glance to calculate a valid_to value with something AddYears():
var valid_to = DateTime.Parse("02/28/2011").AddYears(1); // will yield 2/28/2012
There are two facts in play here:
- The timeframe between 2/28/2011 and 2/28/2012 is 1 year
- 2/29/2012 > 2/28/2012
In no case will AddYears() ever yield 2/29.
To clarify, in no case will AddYears(1) ever yield 2/29. If the current day is 2/29, AddYears(4) will in fact yield 2/29 (assuming of course the target year is in fact a leap year.)
1 year from 2/28/2011 is 2/28/2012 and 1 year from 3/1/2011 is 3/1/2012. 2/29 is a missing data point.
The question is how do you treat that extra day? The answer of course is “It depends!” It depends on how your business rules are setup. Let’s go back to the problem domain in this case. Per its post, MS calculated a valid_to date and that such date was calculated to be 1 year from the date the transfer certificate was calculated. For those certificates generated on 2/28/2011, 2/28/2012, per the language of the specification, was valid. Advancing 1 day, the same would hold true for 3/1. Finally, if the current date is greater than the the valid_to date, the certificate is deemed to be expired. Then, when you factor in the downstream processes enumerated in the explanation, it is clear why things turned out the way it did.
Going back to calculating a valid_to date ahead of time, do we treat 2/29 as a free day? Does something like this make sense?:
DateTime _validTo = Valid_From.AddYears(1); if(_validTo.Month == 2 && _validTo.Day == 28 && DateTime.IsLeapYear(_validTo.Year)) _validTo = _validTo.AddDays(1); return _validTo;
Or, is this the right thing to do?:
DateTime _validTo = Valid_From.AddYears(1); if (_validTo.Month == 3 && _validTo.Day == 1 && DateTime.IsLeapYear(_validTo.Year)) _validTo = _validTo.AddDays(-1); return _validTo;
DateTime _validTo = Valid_From.AddYears(1); if (_validTo.Month == 2; _validTo.Day == 28; DateTime.IsLeapYear(_validTo.Year)); _validTo = _validTo.AddDays(2); //Just advance the valid_to date to 3/1 </span> //and make 2/29 a free day </span> return _validTo;
Clearly, there are several ways the leap day can be handled and such handling is dependent on your business rules. But wait, it can get more interesting. What happens when 2/29/2012 is the valid_from date?. Invoking AddYears(1) on that date will land you on 2/28/2013. Hopefully, we don’t see code like this 😛
var dateEnd = DateTime.Parse(dateStart.Month.ToString() + "/" + dateStart.Day.ToString() + "/" (dateStart.Year + 1).ToString());
Looking at the explanation from Microsoft, this is essentially what happened – adding 1 year to the current date’s year. Not a great way to address the problem.. BUT – just using AddYears() alone would not be enough to address the issue. One still has to confront how the extra day will be treated. IMO, one year from 2/28/2012 is 2/29/2013 (in spite of the fact that AddYears(1) will land you on 2/28/2013. I guess it gets down to the business definition of what a year is. Is it the same date – but for the following year? That can’t be true because 2/29/2013 is an invalid date. Is a year 365 days, except when the following year is a leap year – which would then mean 366 days? Does this mean that DateParse.Parse(“02/28/2011″).AddYears(1) should yield 2/29/2013 and should DateParse.Parse(“02/29/2012″).AddYears(1) yield 3/1/2013?
Another approach would be to not statically calculate the valid_to date. Therefore, something like this would likely always work:
bool valid_= (currentDate.AddYears(-1) >= valid_from);
It depends on whether you are going to dynamially calculate whether something is valid or, for the sake of some performance consideration, ahead of time, you calculate the valid_to date. If the latter, you will need to factor in the leap year issue. If the former, it’s taken care of – albeit at some negligible performance cost. I’d vote for the former and call it a day… Poll the GA’s that, as of this moment in time (something passed to the function) – that have expired certificates.
The take away from this issue – a reminder that if you are dealing with dates, be sure to have tests that contemplate leap years as part of your test coverage. We cannot always assume 365 days in a year. We are aware of the leap year issue. But – when you are heads-down into a task that deals with dates, the leap year issue, as obvious as it may be with 20/20 hindsight may not be as obvious as one would think. To some degree, we are conditioned to think that leap years get accounted for automatically – like Daylight Savings Time. In some contexts, it is. After all, DateTime.Parse(“2/29/2012”) is valid – DateTime.Parse(“2/29/2013”) is not.
The other take away is don’t rush to deploy a solution to production until you have performed the requisite due diligence.
By the way, are leap years every 4 years? In our lifetime, it is.
Actually, a leap year is a year that is evenly divisible by 4, 100 and 400.
Thanks for the astute comment from Julian and Damien – A leap year is a year that is divisible by 4, except when it’s also divisible by 100, …except when it is also divisible by 400.
2096 is a leap year. 2100 is not! Fortunately, we have DateTime.IsLeapYear(year)!!
Thanks for Microsoft’s transparency and candor, this was a teachable moment.
Assumptions can be fickle.