4
\$\begingroup\$

I've written a function that returns how many Months and Days are between two dates. I want to respect calendar month boundaries, but I'm ignoring time as it's not relevant for my needs.

I've got the following, but I feel like I'm missing thing I can do to optimize it/refactor it. So I'm looking for additional eyes to help me review this.

//As opposed to TimeSpan ^_^
public readonly struct CalendarSpan 
{
    public CalendarSpan(int months, int days)
    {
        Months = months;
        Days = days;
    }
    public readonly int Months;
    public readonly int Days;
}

public static CalendarSpan DifferenceInMonthsAndDays(DateTime startDate, DateTime endDate)
{
    //Ensuring that the larger of two dates is the latter argument 
    if (startDate > endDate)
        return DifferenceInMonthsAndDays(endDate, startDate);

    //Start with getting the difference in months
    var months = 12 * (endDate.Year - startDate.Year) + endDate.Month - startDate.Month;

    int days;
    //Add the difference in months to the smaller of the two dates
    var addedMonths = startDate.AddMonths(months);
    if (addedMonths <= endDate)
        //So long as we can avoid negative numbers, just do the simple math
        days = endDate.Day - addedMonths.Day;
    else
    {
        //Otherwise, backoff by 1 months, and use the built in Subtract logic to get the number of days between the two dates
        months--;
        addedMonths = startDate.AddMonths(months);
        days = endDate.Subtract(addedMonths).Days;
    }

    return new CalendarSpan(months, days);
}

UPDATE I have been trying to get unit tests for this, and I honestly don't know why I ever attempted this. It is NOT an easy or straight forward thing to do at all. Insert obligatory meme of Homer disappearing into the hedges

\$\endgroup\$
  • 6
    \$\begingroup\$ This will give the same span for some combinations of dates (e.g. May 30 -> Jun 30 and May 31 -> Jun 30 will both give 1 month 0 days because AddMonths will return the last day of the month if the calculated day does not exist). Is this expected/acceptable behavior? \$\endgroup\$ – 1201ProgramAlarm Jul 25 '19 at 3:09
  • 1
    \$\begingroup\$ You mention as opposed to timespan, but you don't justify why. \$\endgroup\$ – tinstaafl Jul 25 '19 at 3:26
  • 5
    \$\begingroup\$ Can you add some unit tests to show what output you would expect? For example with 30.01.2019 and 01.12.2019 your solution gives 10 months, 1 day but I could imagine that 10 months and 2 days would be equally likely, depending on where you start. \$\endgroup\$ – poke Jul 25 '19 at 10:32
  • 4
    \$\begingroup\$ “It is NOT an easy or straight forward thing to do at all.” – Welcome to the world of calendar dates… 😂 \$\endgroup\$ – poke Jul 26 '19 at 7:45
  • 3
    \$\begingroup\$ I've voted to close this because I think it is impossible to comment on the suitability of the code without a better specification. \$\endgroup\$ – VisualMelon Jul 26 '19 at 12:33
0
\$\begingroup\$

Last answer deleted (instead of substantially revising).

Notes (see comments in code)

  1. CalendarSpan signed (date1 > date2 => negative span)
  2. Extension overload of DateTime.Add() taking CalendarSpan argument
  3. Unit test approach (see Main() which iterates positive and negative differences, using DateTime functions to check integrity of results)

`

using System;

namespace DateDiff
{
  static class Program
  {
    public readonly struct CalendarSpan 
    {
      public CalendarSpan(int months, int days)
      {
        Months = months;
        Days = days;
      }
      public readonly int Months;
      public readonly int Days;
    }

    //  Maintains sign-consistency, i.e. if l > r returned value components are positive and if l < r negative
    public static CalendarSpan MonthDaysDifference(this DateTime l, DateTime r)
    {
      var mDiff = l.Month - r.Month + 12 * (l.Year - r.Year);

      var dDiff = (l - r.AddMonths(mDiff)).Days;
      if (dDiff == 0 //  no day, the span is a whole number of months
          || l > r && dDiff > 0 //  day and month differences ...
          || l < r && dDiff < 0) //  ... have the same sign
        return new CalendarSpan(mDiff, dDiff);
      //  Signs differ
      var dSign = Math.Sign(dDiff);
      var mSign = Math.Sign(mDiff);
      //  It doesn't matter which sign is which, we adjust months in the correct direction
      mDiff += dSign;
      //  and recalculate days
      return new CalendarSpan(mDiff, (l - r.AddMonths(mDiff)).Days);
    }

    public static DateTime Add(this DateTime dt, CalendarSpan span)
    {
      return dt.AddMonths(span.Months).AddDays(span.Days);
    }

    //  Included to suggest how to test
    static void Main(string[] args)
    {
      for (int i = 0; i < 1000; i++)
      {
        var date1 = new DateTime(2017, 12, 25).AddDays(i);
        for (int j = 0; j < 1000; j++)
        {
          var date2 = new DateTime(2019, 01, 01).AddDays(j);
          var dateDiff = date1.MonthDaysDifference(date2);
          //var dateDiff2 = MonthDaysDifference(date1,date2);//just checking
          if (date2.Add(dateDiff) != date1)
          {
            Console.WriteLine($"ERROR: {date1:yyyy-MM-dd} - {date2:yyyy-MM-dd} = {dateDiff.Months} months + {dateDiff.Days} days");
            Console.WriteLine("Any key to exit");
            Console.ReadKey();
            return;
          }
          Console.WriteLine($"{date1:yyyy-MM-dd} - {date2:yyyy-MM-dd} = {dateDiff.Months} months + {dateDiff.Days} days");
        }
      }
    }
  }
}
| improve this answer | |
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.