Interesting Discovery in the .NET System.DateTime Class

Unlike most of my posts here, this one is going to get a little technical.  While working on a new application to display upcoming birthdays from my Outlook contacts on the new Windows Mobile 6.1 sliding panels home screen, I had a bug that was resulting in one birthday on April 29 displaying in the sorted list between two April 30 birthdays.  Wierd! 

Let’s see if you can spot the “feature” in the following code:

 1: public AnniversaryItem(string firstName, string lastName, DateTime date, AnniversaryItemType type, string id)
 2: {
 3:     FirstName = firstName;
 4:     LastName = lastName;
 5:     Date = date;
 6:     ItemType = type;
 7:     Picture = null;
 8:     NumDaysUntilDate = Date.DayOfYear - DateTime.Today.DayOfYear;
 9:     if (NumDaysUntilDate < 0)
 10:         NumDaysUntilDate += 365;
 11:  
 12:     DateTime nextDate = DateTime.Today.AddDays(NumDaysUntilDate);
 13:     Years = nextDate.Year - Date.Year;
 14:  
 15:     OutlookId = id;
 16: }

The relevant line here is line 8.  And here’s where the interesting stuff happens:  What I’m doing here is finding out how many days it will be until the next occurrence of the birthday or anniversary, so I subtracted the DayOfYear property of today from the DayOfYear property of the anniversary then added 365 if the answer was negative. 

So why was April 29 showing up between two April 30 birthdays?  Well it turns out that the DayOfYear property is leap year aware!  The three birthdays were April 29, 1980, April 30, 1980, and April 30, 1990.  In this scenario, 1980 is a leap year.  In the leap year we add a day on February 29.  What happens here is that April 30, 1990 is day 120 of the year 1990.  April 29 of 1980 is day 120 of 1980 and April 30 is day 121 of 1980.  So when you sort by the DayOfYear, you get April 30, 1990, April 29, 1980, and then April 30, 1980.  Very clever on Microsoft’s part and very confusing if you’re not aware of the consequences!

The solution is as follows (lines 9-12):

 1: public AnniversaryItem(string firstName, string lastName, DateTime date, AnniversaryItemType type, string id)
 2: {
 3:     FirstName = firstName;
 4:     LastName = lastName;
 5:     Date = date;
 6:     ItemType = type;
 7:     Picture = null;
 8:     NumDaysUntilDate = Date.DayOfYear - DateTime.Today.DayOfYear;
 9:     if (((DateTime.IsLeapYear(Date.Year) && !DateTime.IsLeapYear(DateTime.Today.Year)) ||
 10:         (!DateTime.IsLeapYear(Date.Year) && DateTime.IsLeapYear(DateTime.Today.Year))) &&
 11:         Date.DayOfYear >= 60)
 12:         NumDaysUntilDate--;
 13:     if (NumDaysUntilDate < 0)
 14:         NumDaysUntilDate += 365;
 15:  
 16:     DateTime nextDate = DateTime.Today.AddDays(NumDaysUntilDate);
 17:     Years = nextDate.Year - Date.Year;
 18:  
 19:     OutlookId = id;
 20: }

What I ended up doing to correct this situation is check to see if one of the two years being compared is a leap year.  In addition, I check to see if the date is after a theoretical February 29.  If one of the two years is a leap year and the date is after February 29, I subtract one from the calculated number of days until the anniversary to correct the sorting issue for the leap year.

Just one more example of why date math can be very challenging!