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\$

closed as unclear what you're asking by dfhwze, esote, Mast, Toby Speight, IEatBagels Aug 12 at 15:46

Please clarify your specific problem or add additional details to highlight exactly what you need. As it's currently written, it’s hard to tell exactly what you're asking. See the How to Ask page for help clarifying this question. If this question can be reworded to fit the rules in the help center, please edit the question.

  • 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 at 3:09
  • 1
    \$\begingroup\$ You mention as opposed to timespan, but you don't justify why. \$\endgroup\$ – tinstaafl Jul 25 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 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 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 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");
        }
      }
    }
  }
}
\$\endgroup\$

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