Years and Months Between Dates

June 22, 2011  |   |  Share
Quick Summary
Calculating the number of years and months between two dates can be tricky. DateSpan to the rescue!

At first glance, calculating the difference between two DateTimes seems easy. Simply subtract the first date from the second and you’ll get a TimeSpan which can be used to get the total number of days elapsed. With the 4th of July right around the corner, let’s calculate the age of the United States since the Declaration of Independence:

DateTime today = new DateTime(2011, 6, 22);
DateTime declarationOfIndependence = new DateTime(1776, 7, 4);
TimeSpan ageTimeSpan = today.Subtract(declarationOfIndependence);
Console.WriteLine("The US is {0} days old.", ageTimeSpan.TotalDays);
The US is 85819 days old.

Unless you are the kind of math whiz that enjoys dividing large numbers in your head, you would probably prefer to see that expressed in years and months instead of total days. While the TimeSpan is great for storing and comparing durations, it isn’t designed for formatting larger time spans. Internally it stores the number of ticks since January 1, 0001. Each tick is 100ns so the TimeSpan provides plenty of precision for most scenarios. On a side note, performance testing is one place where you shouldn’t use a TimeSpan, use the StopWatch class instead. Unfortunately, the TimeSpan doesn’t have any context about when the period took place, so it cannot convert the ticks into anything beyond days. At this point you might be thinking…

Can we just divide the total number of days by 365?

No, because that doesn’t take into account leap years. From our previous example where we calculated that the US is 85,819 days old, dividing by 365 would tell us that the US is 235 years old when it is actually 234 years old as of today (June 22, 2011):

DateTime today = new DateTime(2011, 6, 22);
Console.WriteLine(
    "{0} years old.",
    Math.Floor((today - declarationOfIndependence).TotalDays / 365));
235 years old. (incorrect)

How about dividing by 365.25?

More accurate, but still flawed. Leap years occur every four years, but also during a year evenly divisible by 400. The average year in the Gregorian calendar is actually 365.2425 days. Even this is slightly off as the solar year is more precisely 365.2422 days, but the difference is not enough to matter in most cases.

So we should use one of these more precise values?

No, using an average like 365.2425 is fine when dealing with larger time periods, but it fails when dealing with short time periods. As an example, imagine you are displaying the “age” of a user since he/she joined the community:

DateTime today = new DateTime(2011, 6, 22);
Console.WriteLine(
    "Joined {0} years ago.",
    Math.Floor((today - new DateTime(2008, 6, 22)).TotalDays / 365.2425));
Joined 2 years ago. (incorrect)

The calculation will be wrong when the total is close to an boundary, but the period doesn’t include a Feb 29 leap day. This error might be acceptable in some cases, but it only gives us the number of years. What if we want to express the time in months or days? There are lots places where users appreciate a nicely formatted duration. I encountered this problem while working on my current project, a comparison shopping engine named FilterPlay. We wanted to include a quick summary of how long ago a product was released. Here’s a snippet of the basic info we show for the Nikon D5100 SLR Camera:

To calculate the ages, I created a struct named DateSpan which stores the time between a beginning and end date as years, months, and days. Because we are using the specific dates, we can account for any leap days so the DateSpan won’t suffer from the inaccuracies that the TimeSpan transform does. Here is the constructor:

public DateSpan(DateTime startDate, DateTime endDate)
{
    if (endDate < startDate)
        throw new ArgumentOutOfRangeException("endDate must occur after startDate");
 
    // initially assume that the end year, month, and day are >= start values
    int years = endDate.Year - startDate.Year;
    int months = endDate.Month - startDate.Month;
    int days = endDate.Day - startDate.Day;
 
    // adjust if the end month occurs before the start month, ex: April 2009 - Feb 2010
    if (endDate.Month < startDate.Month)
    {
        years--;
        months += MONTHS_PER_YEAR;
    }
 
    // adjust if the end day occurs before the start day, ex: April 30 - May 25
    if (endDate.Day < startDate.Day)
    {
        // The last day of the month is equal to any larger day following the
        // behavior of DateTime.AddMonths: March 31 + 1 month = April 30
        int endMonthDays = DateTime.DaysInMonth(endDate.Year, endDate.Month);
        if (endDate.Day == endMonthDays && startDate.Day > endMonthDays)
        {
            days = 0;
        }
        else
        {
            // remove one month
            if (months > 0)
            {
                months--;
            }
            else
            {
                years--;
                months = MONTHS_PER_YEAR - 1;
            }
 
            // get the number of days in the previous month
            int previousMonthDays = (endDate.Month > 1)
                ? DateTime.DaysInMonth(endDate.Year, endDate.Month - 1)
                : DateTime.DaysInMonth(endDate.Year - 1, MONTHS_PER_YEAR);
 
            // sum remaining days in previous month with the days in the current month
            days = previousMonthDays - Math.Min(startDate.Day, previousMonthDays) +
                endDate.Day;
        }
    }
 
    _years = (ushort)years;
    _months = (byte)months;
    _days = (byte)days;
}

The one tricky part in this code is dealing with the varying length of months. Most people would consider the span Feb 28 – Mar 31 as 1 month, 3 days. What about Mar 31 – Apr 30? How about Feb 29 (during a leap year) to Feb 28 (in a non-leap year)? There is no absolute correct answer here, but fortunately the framework already has a position on this issue. I’ve adopted the same behavior as described in the documentation for DateTime.AddMonth:

The AddMonths method calculates the resulting month and year, taking into account leap years and the number of days in a month, then adjusts the day part of the resulting DateTime object. If the resulting day is not a valid day in the resulting month, the last valid day of the resulting month is used. For example, March 31st + 1 month = April 30th.

Here is an example using the DateSpan to calculate the length of the War in Iraq.

DateTime warBegan = new DateTime(2003, 3, 20);
DateTime warEnded = new DateTime(2010, 8, 31);
DateSpan warLength = new DateSpan(warBegan, warEnded);
Console.WriteLine("The Iraq War lasted {0}.", warLength.ToString());
The Iraq War lasted 7 years, 5 months, 11 days.

While the DateSpan is perfect for displaying longer time periods, it should not be used to compare different time periods. Comparisons might not be accurate because years and months vary in he amount of time they represent. For example June 1 – June 30 and July 1 – July 31 would both be represented as 1 month using a DateSpan, but July has one more day than June.

Formatting the DateSpan

The DateSpan overrides the ToString() method and by default will print any non-zero portions of the duration. Singular and plural units are used where appropriate. I also added a ToShortString() method which only includes the pieces which represent at least 25% of the total duration. Here are some examples of the two types of formatting:

Years Months Days ToString() ToShortString()
2 0 1 2 years, 1 day 2 years
1 5 10 1 year, 5 months, 10 days 1 year, 5 months
0 1 1 1 month, 1 day 1 month
0 1 20 1 month, 20 days 1 month, 20 days

Of course, you are free to modify the formatting to fit your needs. The full source code for the DateSpan struct along with some unit tests is available in my GitHub library:



Leave a Reply