Introduction

There have been numerous times that I have been asked to provide a threshold or measure something in months. I always warn that this is a bad idea, but quickly I am over ruled because wah wah we want it that way. The warning falls on deaf ears until they see the error of their ways. Months is a terrible measurement and I am going to prove it to you here with a demonstration.

Numerous Gotchas

Have you ever noticed that the TimeSpan structure from C# doesn’t have a months property? This isn’t a mistake, it’s not something they forgot to develop, it is because measuring anything in months is a terrible idea unless:

  1. you are willing to accept inaccurate results depending on what year it is
  2. you are okay with using months as a decimal and NOT an integer
    1. All of your trouble begins the moment you want an integer
    2. If you are okay with a decimal, then this isn’t a big deal because you are providing proper precision

Fallacies of counting months

People often make mistakes when counting months by making assumptions about months. For some reason they fail to think about what a month actually is. A month is a container of days that isn’t consistent. The moment there is an inconsistency you really shouldn’t use it to measure anything. Would you use a ruler that sometimes tells you that 12 inches is really 8 depending on the time of day? Then why would you use months?

  1. There aren’t 30 days in a month, this is just flat out wrong and trying to compensate for each month’s individual rules isn’t the greatest idea. A lot of complexity for the little bit of gain that it will provide just doesn’t seem worth it.
  2. Depending on the year there could be one more day in February due to leap year, meaning usually it is 28 days, but during a leap year it is 29 days.
  3. What is considered a complete month? The fact that you reach the beginning of a month or the end of a month? This comes down to how you want to approach your count, is it inclusive or an exclusive count?
    1. Example A: If we count from 01/01/0001 to 02/01/0001 this is one month
    2. Example B: If we count from 01/01/0001 to 02/20/0001 this is still one month if you are using integers because you haven’t gotten to 03/01/0001 or 02/28/0001 (or 02/29/0001 for leap year) depending on how you want to count it.
      1. The only way to handle partial months is to use decimals so you get fractions of a month, meaning the month isn’t over
      2. Rounding is a terrible idea in this instance, don’t do it because it will cause issues – large trust issues especially when it comes to legal documents.
  4. If you really wanted to, you could include the time component, but I really don’t see the point of this. At that point it is just extra anal to be paying attention to the time component which is why you need to decide are you counting to the next month or the last day of the month.

Counting months code example

The following is a full fledged code example demonstrating how inaccurate counting months can be depending on how you want to handle it. In my code I am literally counting months, but also explicitly removing the time component because we have enough complexity with the date component – again this is a choice.

Here a GitHub link to the same code.

void Main()
{
	CountMonthsAndPrint("2018-04-01", "2018-04-01");
	CountMonthsAndPrint("2018-04-01", "2018-04-02");
	CountMonthsAndPrint("2018-04-01", "2018-04-30");
	CountMonthsAndPrint("2018-04-01", "2018-05-01");
	CountMonthsAndPrint("2018-04-01", "2018-06-01");
	CountMonthsAndPrint("2018-04-01", "2018-06-15");
}

public void CountMonthsAndPrint(string start, string end)
{
	var m = CountMonthsBetweenDates(start, end);

	Console.WriteLine($"{start} to {end} = {m} months");
}

public int CountMonthsBetweenDates(string start, string end)
{
	var dtmStart = TryParseDateTimeString(start, "Start");
	var dtmEnd = TryParseDateTimeString(end, "End");

	return CountMonthsBetweenDates(dtmStart, dtmEnd);
}

public int CountMonthsBetweenDates(DateTime start, DateTime end)
{
	//Kill the time component
	var s = start.Date;
	var e = end.Date;

	var i = 0;

	for (var d = s; d < e; d = d.AddMonths(1))
	{
		i++;
	}

	return i;
}

public DateTime TryParseDateTimeString(string dateTimeString, string label)
{
	if (!DateTime.TryParse(dateTimeString, out var dtm))
		throw new Exception($"{label} date is using an invalid format: {dateTimeString}");

	return dtm;
}

Output of the above example

If you want to see it for yourself or play around with the script you can run the above in LinqPad or in whatever IDE you want. Here is the output of the above code so you don’t have to run it at this exact moment:

2018-04-01 to 2018-04-01 = 0 months
2018-04-01 to 2018-04-02 = 1 months
2018-04-01 to 2018-04-30 = 1 months
2018-04-01 to 2018-05-01 = 1 months
2018-04-01 to 2018-06-01 = 2 months
2018-04-01 to 2018-06-15 = 3 months
You can argue with the validity of what is shown, but that’s the point – to show you that this problem can be attacked too many ways. This is why Microsoft doesn’t provide a Months property as part of its TimeSpan structure. Essentially – it’s up to you as to how you want to interpret it because there will always be that doubt “How did they calculate this?”
In my code example you can see that anything past one day from the starting date is considered a month, which is pretty stupid when it comes down to it. Again this is a question of how you want to hack it.

Tread carefully with your decisions

Depending on what you are working on, how you do this doesn’t matter as long as the users aren’t relying on this information for strong precision (or hell even accuracy really). If you are calculating things that affect money, then you need to get down to specifics and exercise all aspects of this calculation. To revisit the fallacies:
  1. Do you need to consider the time component? This is very unlikely and I recommend not paying attention to this, it really has little impact, so why bother?
  2. Choose your fuzzy logic and stick with it by making commanding statements:
    1. We are defining a whole month as:
      1. Counting from the first of month A until the first of month B using integers
      2. Counting from the first of month A until the last day of month A using integers
      3. Counting from the first of month A until the first of month B using using decimals meaning instead of showing you zero months have passed, show a fraction of a month such as 0.75 months or 1.30 months. Remember – DON’T round these values because 0.75 is NOT 1 month, just don’t do that.

I have had to experience this problem with calculating:

  • Budgets
  • Estoppel certificates
  • Contracts
If you aren’t careful something as sensitive as an estoppel certificate can blow up and cost your company money if sums aren’t calculated correctly. People fail to see how serious this is until it is too late, so make sure you explain to your business people why using months is terrible.

If you hate months so much what would you use instead?

Days – just count days and be done with it man. If you are still demanded to display months, then please try to calculate the number of months but include a decimal for the clarity of a partial month.

Leave a Reply

Your email address will not be published. Required fields are marked *