2/29/****: The case of the missing data point and the curious thing a Leap Year is

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 &&
   _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 && 
   _validTo = _validTo.AddDays(-1);
return _validTo;

Or this?

DateTime _validTo = Valid_From.AddYears(1);
if (_validTo.Month == 2;
   _validTo.Day == 28;
   _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.

About johnvpetersen

I've been developing software for 20 years, starting with dBase, Clipper and FoxBase + thereafter, migrating to FoxPro and Visual FoxPro and Visual Basic. Other areas of concentration include Oracle and SQL Server - versions 6-2008. From 1995 to 2001, I was a Microsoft Visual FoxPro MVP. Today, my emphasis is on ASP MVC .NET applications. I am a current Microsoft ASP .NET MVP. Publishing In 1999, I wrote the definitive whitepaper on ADO for VFP Developers. In 2002, I wrote the Absolute Beginner’s Guide to Databases for Que Publishing. I was a co-author of Visual FoxPro Enterprise Development from Prima Publishing with Rod Paddock, Ron Talmadge and Eric Ranft. I was also a co-author of Visual Basic Web Development from Prima Publishing with Rod Paddock and Richard Campbell. Education - B.S Business Administration – Mansfield University - M.B.A. – Information Systems – Saint Joseph’s University - J.D. – Rutgers University School of Law (Camden) In 2004, I graduated from the Rutgers University School of Law with a Juris Doctor Degree. I passed the Pennsylvania and New Jersey Bar exams and was in private practice for several years – concentrating transactional and general business law (contracts, copyrights, trademarks, independent contractor agreements, NDA’s, intellectual property and mergers and acquisitions.).
This entry was posted in Azure, Leap Year Calculations. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Pingback: Non overlapping time periods–because I like the pain of 2 AM wakeup calls - Ayende @ Rahien

  • Anonymous

    Point taken about noticing a different day.

    I don’t think this is a technology problem, specifically. Government agencies have been using 30, 60, 90 days rather than months to avoid ambiguity for a long time.

    I just wanted to point out that this is a symptom of imprecise definitions. A clerk behind a desk would have as to apply some interpretation, just as your article does not proscribe a single disambiguating rule.

  • Dan Puzey

    I think any customer is going to notice if their “4 year” contract ends on a day different to the one it starts on. Even more so if a 6 month contract ends a week out because you’ve rounded months to 30 days!

    This is a wider problem than just MS’s internal certificates. Computing – business in general – is full of cases where the humanised business rules don’t fit neat technical solutions. In each case, it’s our job to make it work – not to redefine business rules to suit the machine.

    And no, I’m not talking about my own users – but I’d certainly be aware/conversant/pedantic enough to notice if a 3-month contract purchased on July 1st didn’t finish on September 31st. As neat and simple as it would be in code, I don’t know of any business that would let me fix the length of their month to anything other than the calendar length.

  • Anonymous

    That also depends on how well you communicate whether the end is a “to” date or a “through” date.

    Congrats if you have users that are aware/conversant/pedantic enough to notice that degree of subtlety, though!

  • Dan Puzey

    That still leaves you at the mercy of “What is the result of DateTime.Parse(“29 Feb 2012″).AddYears(1)?”

    Try this in .NET:

    DateTime.Parse(“28 Feb 2012″).AddDays(1).AddYears(1).AddDays(-1)

    And you’ll get back 27 Feb 2013.

  • Dan Puzey

    It’d be a surprise to a user: if I buy something that expires in 4 years, I’d expect it to expire on this date 4 years from now – not a day earlier because a system has missed out the leap-day.

  • Anonymous

    This is why business rules shouldn’t be defined in inexact measures like years & months, but in precise terms like days.

    .AddDays(365) and no surprises.

  • Anonymous

    [DST pedantry] “Daylight Saving Time”, not “Daylight Savings Time”.

  • Garry Shutler

    And by “most the time” I don’t mean 47 months out of 48. Usually you just have an end date and you have either passed it or you haven’t. Not quite sure how you would write something that made 29th Feb a black hole.

  • Garry Shutler

    date.AddDays(1).AddYears(1).AddDays(-1) if this is a problem. Most the time you can get away without it though.

  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1064

  • Anonymous

    Great analysis. I agree that AddYears(1) doesn’t solve the problem. But exclusive end dates do. It’s a shame that this practice is not common in software.

  • Anonymous

    In no case when you are calling AddYears(1). I should have clarified that. You are indeed correct that adding 4 years to 2/29 will yield 2/29 . Thanks.

  • Anonymous

    “In no case will AddYears() ever yield 2/29″ – even AddYears(4)?

  • Anonymous

    You are right.. :-) I’ll fix that.

  • Julian Bucknall

    John: “a leap year is a year that is evenly divisible by 4, 100 and 400″. Not quite. 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. So 2000 was a leap year, 2100, 2200, 2300 won’t be, but 2400 will be (if the human race is still around…).

    Cheers, Julian

  • Damien

    “Actually, a leap year is a year that is evenly divisible by 4, 100 and 400″ needs re-phrasing – it implies that leap years only happen every 400 years (and the first two conditions are redundant). What (I hope) you mean is divisible by four but not divisible by 100, unless it’s divisible by 400.