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));

Thursday, September 15, 2011

ASP.NET Substitution Control and Output Caching


Ever want to use Output Caching to cache a page, but you didn't because there were some bits of the page that you just couldn't cache?

ASP.NET has a Substitution control that lets you handle that scenario...

At the top of your page, you might have this...
<%@ OutputCache Duration="10" VaryByParam="none" %>


Somewhere later in the page, you could have this...
<asp:Substitution runat="server" ID="sub" MethodName="GetTime"/>


And in your code-behind, you could have this...
public static string GetTime(HttpContext ctx)
{
    return DateTime.Now.ToString();
}

The entire page will stay cached for 10 seconds...except for the substitution control, which will update every time you refresh the page. I love it!

Linq Aggregate - one liner for comma separated lists

Here's a cool Linq function for creating a comma-separated list from a collection of strings... It even handles the case where there's only one thing in the list, and it doesn't add a final comma at the end...pretty slick!

myList.Aggregate((s1,s2) => s1 + ", " + s2)

Wednesday, July 27, 2011

LinqPad is my new best friend

LinqPad is my new best friend. To get the best goosebumps when you use it, you need to purchase the auto-complete feature. Oh wow. This thing is awesome. I can write linq queries, lambda expressions, or even t-sql if I want against a database. I can find exactly the information I need before putting the query into my app. This saves me tons of time. I don't have to wait for a recompile each time to see if my linq expression will return the values I want. Also, the Visual Studio debugger isn't lambda expression friendly, so I'm overjoyed with LinqPad.

Thursday, May 5, 2011

Silverlight Spy is awesome!

Silverlight Spy is awesome! I'm a huge fan of FireBug for debugging web applications. Silverlight Spy is basically the same thing for Silverlight apps. The free version lets you inspect any Silverlight app. The paid version lets you change properties of objects on the fly and see the changes right on the screen. I'm in UI debug heaven. It does more things too. I haven't tried the advanced features yet, like the Event and Performance monitors.

Tuesday, April 5, 2011

Silverlight development

I'm on day 2 of Silverlight development, and have to admit, this is one of the coolest technologies I've ever had the pleasure of playing with...so much fun.

Tuesday, February 8, 2011

Cross-branch merges in TFS, yes it's possible!

I've run across a scenario where I need to do a merge from one branch to another branch that's neither the child nor parent of the source branch.
Trunk------
     \
      \Branch one------
               \
                \Branch two------
This is an odd scenario, and we got here by mistake, but I need to make changes to Branch two, then merge the changes into the Trunk without affecting Branch one. No further development is being performed in Branch one.

This cannot be performed through the Visual Studio TFS graphical interface, but you can launch this type of merge from the command line. See this article about baseless merges.

Here's what to do and what happens. From a VS command prompt...
tf merge /baseless c:\data\proj1 c:\data\proj2 /recursive /version:C123~C125
The first path is the source branch; the second is the destination.
The version "C" stands for changeset, and the changeset numbers follow. You can give a range or make the numbers the same to do a single changeset.

This will do the merge and present you with the graphical merge dialog you're used to seeing. I suggest using the merge tool to merge changes manually so you get only the changes you want. Auto-merge may bring in changes you didn't intend.

This just saved me a lot of time today.