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:
- you are willing to accept inaccurate results depending on what year it is
- you are okay with using months as a decimal and NOT an integer
- All of your trouble begins the moment you want an integer
- 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?
- 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.
- 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.
- 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?
- Example A: If we count from 01/01/0001 to 02/01/0001 this is one month
- 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.
- The only way to handle partial months is to use decimals so you get fractions of a month, meaning the month isn’t over
- 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.
- 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
Tread carefully with your decisions
- 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?
- Choose your fuzzy logic and stick with it by making commanding statements:
- We are defining a whole month as:
- Counting from the first of month A until the first of month B using integers
- Counting from the first of month A until the last day of month A using integers
- 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.
- We are defining a whole month as:
I have had to experience this problem with calculating:
- Budgets
- Estoppel certificates
- Contracts