Wednesday, December 21, 2011

c# Calculating number of full months between 2 dates

I've come across a weird requirement that wishes to calculate the number of full months between 2 dates. Like anything else, you can't (shouldn't) really start coding the solution until you understand the problem.

First off, the questions:
  1. When you say full month do you mean 30 days? Some months have 31, and one has 28 or 29.
  2. Is the 15th of one month to the 15th of the next month considered a full month, or is it not until the 16th of the next month?
  3. In your vision of how this will go, how many full months would you say lie between the following dates?
    • 1/31/2011 to 2/28/2011
    • 1/15/2011 to 2/15/2011
    • 1/15/2011 to 2/16/2011
    • 6/30/2011 to 7/31/2011
  4. Is this calculation already performed in other parts of the system or in other systems with which this one shares data?
In my case, it turned out that #4 was the answer, but I wasn't satisfied with the simplistic and not-so-logical calculation. We were already doing this calculation in SQL server, and I could simply do the same calculation in c#.

But, before finding this out, I tried to wrap my brain around what would be a good logical answer. I knew we weren't assuming any number of days to be a full month, and I liked the idea of the 15th of one month to the 15th of the next month constituting one full month. Or the 2nd to the 2nd, etc. But...think about edge cases, specifically the end of the month. In the simple case, let's assume that 7/31 to 8/31 is one full month. Makes sense to me. What about 1/31 to 2/28? February doesn't have 31 days, so there's no 2/31; and, we're not assuming any number of days when we consider what a full month is. So I propose an algorithm like this...

Write a method that takes a start date as a parameter, and returns the date one full month later.
  • Using "1/31/2011" as our start date, build the next month's date as a string of "2/31/2011"
  • Use DateTime.TryParse to see if our string is a date.
  • If it is not, subtract one from the day and try it again, so "2/30/2011"...nope still not a date
  • "2/29/2011"...not this year, still not a date
  • "2/28/2011" is a valid date; therefore this is one full month after "1/31/2011". This makes sense to me as we're going from the last day of one month to the last day of the next month.
Here's the code for that
private static DateTime GetNextMonth(DateTime date)
{
  var month = date.Month;
  var day = date.Day;
  var year = date.Year;

  var nextDateMonth = month == 12 ? 1 : month + 1;
  var nextDateYear = month == 12 ? year + 1 : year;

  DateTime nextDate;

  while (!DateTime.TryParse(nextDateMonth + "/" + day 
    + "/" + nextDateYear, out nextDate))
  {
    // if it didn't parse right, 
    // then the month must not have that many days
    day--;
  }

  return nextDate;
}

Now, write another method that takes 2 dates and returns the number of full months between them.
  • The input parameters will be startDate and endDate
  • First, make sure startDate is before endDate; if not, swap them
  • Initialize a count variable to 0
  • Now, starting with the startDate, get the date of the next full month by calling the GetNextMonth method
  • if it's still less than the end date, increment the count variable
  • while the next full month is still less than the end date, continue to do this
Here's the code for that
private static int GetMonthDifference(DateTime startDate,
                                      DateTime endDate)
{
  // if dates were passed in wrong order, swap 'em
  if (startDate > endDate)
  {
    var temp = startDate;
    startDate = endDate;
    endDate = temp;
  }

  var count = 0;
  var tempDate = startDate;

  while ((tempDate = GetNextMonth(tempDate)) <= endDate)
  {
    count++;
  }

  Console.WriteLine("From {0} to {1} is {2} month{3}.", 
    startDate.ToShortDateString(), endDate.ToShortDateString(), 
    count, count == 1 ? "" : "s");

  return count;
}

This may not be the tightest, most elegant algorithm, in fact it's kind of brute force, but considering the frequency of its use, it's good enough. First off, the calculation is hardly ever done. Second of all, the distance between the start and end dates is often less than a year, so it won't iterate more than a handful of times on any given call.

Another cool thing to do would be to create an extension method for this...or come up with a more efficient algorithm just for fun.

For completeness, here's a sample call to GetMonthDifference

var months = GetMonthDifference(new DateTime(2011, 1, 31), new DateTime(2011, 2, 27));