DateTimeOffset Same Day comparison

6.9k views Asked by At

I ran into interesting issue with the following requirement: Test if a process had run in the same day, if not run the process. The dates are stored as DataTimeOffset.

My original approach was to:

  1. Convert both values to UTC, because these dates could have been created in different time zones and have different offsets.
  2. View the Date value of each value. This is done after converting to UTC because the Date method ignores the offset.

Most scenarios this worked but I came across one case that the logic would fail. If one of the values had a time that was close to the previous/next day so that the when converting to UTC it would change the date. If the other value didn't have a time that also converted to the previous/next day then the date comparison failed.

So I ended up with the following logic to include that scenario:

public static bool SameDate(DateTimeOffset first, DateTimeOffset second)
{
    bool returnValue = false;
    DateTime firstAdjusted = first.ToUniversalTime().Date;
    DateTime secondAdjusted = second.ToUniversalTime().Date;

    // If date is now a day ahead after conversion, than add/deduct a day to other date if that date hasn't advanced
    if (first.Date < firstAdjusted.Date && second.Date == secondAdjusted.Date)
        secondAdjusted = secondAdjusted.Date.AddDays(1);
    if (first.Date > firstAdjusted.Date && second.Date == secondAdjusted.Date)
        secondAdjusted = secondAdjusted.Date.AddDays(-1);

    if (second.Date < secondAdjusted.Date && first.Date == firstAdjusted.Date)
        firstAdjusted = firstAdjusted.Date.AddDays(1);
    if (second.Date > secondAdjusted.Date && first.Date == firstAdjusted.Date)
        firstAdjusted = firstAdjusted.Date.AddDays(-1);

    if (DateTime.Compare(firstAdjusted, secondAdjusted) == 0)
        returnValue = true;

    return returnValue;
}

Here is the Unit Tests that were failing that now pass:

 [TestMethod()]
 public void SameDateTest()
 {
 DateTimeOffset current = DateTimeOffset.Now;
 DateTimeOffset first = current;
 DateTimeOffset second = current;

 // 23 hours later, next day, with negative offset (EST) -- First rolls over
 first = new DateTimeOffset(2014, 1, 1, 19, 0, 0, new TimeSpan(-5, 0, 0));
 second = new DateTimeOffset(2014, 1, 2, 18, 0, 0, new TimeSpan(-5, 0, 0));
 Assert.IsFalse(Common.SameDate(first, second));

 // 23 hours earlier, next day, with postive offset -- First rollovers
 first = new DateTimeOffset(2014, 1, 1, 4, 0, 0, new TimeSpan(5, 0, 0));
 second = new DateTimeOffset(2014, 1, 2, 5, 0, 0, new TimeSpan(5, 0, 0));
 Assert.IsFalse(Common.SameDate(first, second));

 // 23 hours later, next day, with negative offset (EST) -- Second rolls over
 first = new DateTimeOffset(2014, 1, 2, 18, 0, 0, new TimeSpan(-5, 0, 0));
 second = new DateTimeOffset(2014, 1, 1, 19, 0, 0, new TimeSpan(-5, 0, 0));
 Assert.IsFalse(Common.SameDate(first, second));

 // 23 hours earlier, next day, with postive offset -- Second rolls over
 first = new DateTimeOffset(2014, 1, 2, 5, 0, 0, new TimeSpan(5, 0, 0));
 second = new DateTimeOffset(2014, 1, 1, 4, 0, 0, new TimeSpan(5, 0, 0));
 Assert.IsFalse(Common.SameDate(first, second));
}

My gut feeling is that there is a cleaner approach than to increment/decrement based on the other value. Is there a better approach?

The primary criteria:

  1. Adjust the both dates to have the same offset.
  2. Return true only if both first and second dates occur in the same calendar day, not within 24 hours.
5

There are 5 answers

6
martijn On BEST ANSWER

Adjust the one of the dates for the difference in both dates:

public static bool SameDate(DateTimeOffset first, DateTimeOffset second)
{
    bool returnValue = false;
    DateTime firstAdjusted = first.ToUniversalTime().Date;
    DateTime secondAdjusted = second.ToUniversalTime().Date;

    // calculate the total diference between the dates   
    int diff = first.Date.CompareTo(firstAdjusted) - second.Date.CompareTo(secondAdjusted);
    // the firstAdjusted date is corected for the difference in BOTH dates.
    firstAdjusted = firstAdjusted.AddDays(diff);

    if (DateTime.Compare(firstAdjusted, secondAdjusted) == 0)
        returnValue = true;

    return returnValue;
}

In this function I am asuming that the offset will never be more than 24 hours. IE the difference between a date and it's adjusted date will not be two or more days. If this is not the case, then you can use time span comparison.

1
Peter Wone On

The general methodology you describe (convert to common time-zone then compare date portion) is reasonable. The problem here is actually one of deciding on the frame of reference. You have arbitrarily chosen UTC as your frame of reference. At first gloss it doesn't matter so long as they are compared in the same time zone, but as you have found this can put them on either side of a day boundary.

I think you need to refine your specification. Ask yourself which of the following you are trying to determine.

  • Whether the values occur on the same calendar day for a specified time zone.
  • Whether the values are no more than 12 hours apart (+/- 12hrs is a 24hr period).
  • Whether the values are no more than 24 hours apart.

It might also be something else. The definition as implemented (but rejected by you) is "Whether the values occur on the same UTC calendar day".

3
Simon Mourier On

What about this function:

public static bool SameDate(DateTimeOffset first, DateTimeOffset second)
{
    return Math.Abs((first - second).TotalDays) < 1;
}

You can substract two dates (DateTimeOffset is smart and knows the timezone) and it will give you a range, a timespan. Then you can check if this range is +- 1 day.

3
StarPilot On

First off, you have an error in your UnitTest.

    [TestMethod()]
    public void SameDateTest()
    {
        DateTimeOffset current = DateTimeOffset.Now;
        DateTimeOffset first = current;
        DateTimeOffset second = current;

        // 23 hours later, next day, with negative offset (EST) -- First rolls over
        first = new DateTimeOffset( 2014, 1, 1, 19, 0, 0, new TimeSpan( -5, 0, 0 ) );
        second = new DateTimeOffset( 2014, 1, 2, 18, 0, 0, new TimeSpan( -5, 0, 0 ) );
        Assert.IsTrue( DateTimeComparison.Program.SameDate( first, second ) );

        // 23 hours earlier, next day, with positive offset -- First rollovers
        first = new DateTimeOffset( 2014, 1, 1, 4, 0, 0, new TimeSpan( 5, 0, 0 ) );
        second = new DateTimeOffset( 2014, 1, 2, 5, 0, 0, new TimeSpan( 5, 0, 0 ) );
        Assert.IsFalse( DateTimeComparison.Program.SameDate( first, second ) );

        // 23 hours later, next day, with negative offset (EST) -- Second rolls over
        first = new DateTimeOffset( 2014, 1, 2, 18, 0, 0, new TimeSpan( -5, 0, 0 ) );
        second = new DateTimeOffset( 2014, 1, 1, 19, 0, 0, new TimeSpan( -5, 0, 0 ) );
        Assert.IsTrue( DateTimeComparison.Program.SameDate( first, second ) );

        // 23 hours earlier, next day, with positive offset -- Second rolls over
        first = new DateTimeOffset( 2014, 1, 2, 5, 0, 0, new TimeSpan( 5, 0, 0 ) );
        second = new DateTimeOffset( 2014, 1, 1, 4, 0, 0, new TimeSpan( 5, 0, 0 ) );
        Assert.IsFalse( DateTimeComparison.Program.SameDate( first, second ) );
    }

This is the corrected test. Your first test should return "True", as should your third posted tests. Those DateTimeOffsets being compared are on the same UTC date. Only test case two and four should return "False", as those DateTimeOffsets are in fact on 2 different dates.

Second, you can simplify your SameDate() function to this:

    public static bool SameDate( DateTimeOffset first, DateTimeOffset second )
    {
        bool returnValue = false;
        DateTime firstAdjusted = first.UtcDateTime;
        DateTime secondAdjusted = second.UtcDateTime;

        if( firstAdjusted.Date == secondAdjusted.Date )
            returnValue = true;

        return returnValue;
    }

As all you are interested in is if first.Date and second.Date are actually on the same UTC date, this will get the job done without an extra cast/conversion to UTC.

Third, you can test your test cases using this complete program:

using System;

namespace DateTimeComparison
{
   public class Program
   {
       static void Main( string[] args )
       {
           DateTimeOffset current = DateTimeOffset.Now;
           DateTimeOffset first = current;
           DateTimeOffset second = current;

           // 23 hours later, next day, with negative offset (EST) -- First rolls over
           first = new DateTimeOffset( 2014, 1, 1, 19, 0, 0, new TimeSpan( -5, 0, 0 ) );
           second = new DateTimeOffset( 2014, 1, 2, 18, 0, 0, new TimeSpan( -5, 0, 0 ) );
           if( false == SameDate( first, second ) ) {
               Console.WriteLine( "Different day values!" );
           } else {
               Console.WriteLine( "Same day value!" );
           }

           // --Comment is wrong -- 23 hours earlier, next day, with positive offset -- First rollovers
           first = new DateTimeOffset( 2014, 1, 1, 4, 0, 0, new TimeSpan( 5, 0, 0 ) );
           second = new DateTimeOffset( 2014, 1, 2, 5, 0, 0, new TimeSpan( 5, 0, 0 ) );
           if( false == SameDate( first, second ) ) {
               Console.WriteLine( "Different day values!" );
           } else {
               Console.WriteLine( "Same day value!" );
           }

           // 23 hours later, next day, with negative offset (EST) -- Second rolls over
           first = new DateTimeOffset( 2014, 1, 2, 18, 0, 0, new TimeSpan( -5, 0, 0 ) );
           second = new DateTimeOffset( 2014, 1, 1, 19, 0, 0, new TimeSpan( -5, 0, 0 ) );
           if( false == SameDate( first, second ) ) {
               Console.WriteLine( "Different day values!" );
           } else {
               Console.WriteLine( "Same day value!" );
           }


           // --Comment is wrong --  23 hours earlier, next day, with positive offset -- Second rolls over
           first = new DateTimeOffset( 2014, 1, 2, 5, 0, 0, new TimeSpan( 5, 0, 0 ) );
           second = new DateTimeOffset( 2014, 1, 1, 4, 0, 0, new TimeSpan( 5, 0, 0 ) );
           if( false == SameDate( first, second ) ) {
               Console.WriteLine( "Different day values!" );
           } else {
               Console.WriteLine( "Same day value!" );
           }
       }

       public static bool SameDate( DateTimeOffset first, DateTimeOffset second )
       {
           bool returnValue = false;
           DateTime firstAdjusted = first.UtcDateTime;
           DateTime secondAdjusted = second.UtcDateTime;

           if( firstAdjusted.Date == secondAdjusted.Date )
               returnValue = true;

           return returnValue;
       }          
    }
}

Set a break point wherever you like and run this short program in the debugger. This will show you that test case 2 and test case 4 are in fact more than 2 days apart by UTC time and therefore should expect false. Furthermore, it will show test case 1 and test case 3 are on the same UTC date and should expect true from a properly functioning SameDate().

If you want your second and fourth test cases to be 23 hours apart on the same date, then for test case two, you should use:

        // 23 hours earlier, next day, with positive offset -- First rollovers
        first = new DateTimeOffset( 2014, 1, 2, 4, 0, 0, new TimeSpan( 5, 0, 0 ) );
        second = new DateTimeOffset( 2014, 1, 1, 5, 0, 0, new TimeSpan( 5, 0, 0 ) );
        Assert.IsTrue( DateTimeComparison.Program.SameDate( first, second ) );

And for test case four, you should use:

        // 23 hours earlier, next day, with positive offset -- Second rolls over
        first = new DateTimeOffset( 2014, 1, 2, 5, 0, 0, new TimeSpan( 5, 0, 0 ) );
        second = new DateTimeOffset( 2014, 1, 3, 4, 0, 0, new TimeSpan( 5, 0, 0 ) );
        Assert.IsTrue( DateTimeComparison.Program.SameDate( first, second ) );
1
Mormegil On

First of all, you need to clear up some confusion what the program should do exactly. For two general timestamps in two general time zones (two DateTimeOffset instances without specific limitations), there is no such concept as “the calendar day”. Each time zone has its own calendar day. For instance, we could have two instances of DateTimeOffset, named first and second, and they have different offsets. Let’s visualize the time axis, and mark the specific time instants to which the DateTimeOffset instances refer with * and the calendar day in the respective time zone (i.e. the interval between 0:00 and 23:59 in the specific timezone) with |__|. It could look like this:

first:  ........|___________________*__|.......
second: ...|______*_______________|............

When in the timezone of first, the second event happened during the same calendar day (between 2–3 am). When in the timezone of second, the first event happened during the following calendar day (between 1–2 am).

So it is obvious the question needs clarification and probably a bit of scope limitation. Are those really generic timezones, or are they timezones of the same place, differing potentially only in the daylight saving time? In that case, why don’t you just ignore the timezone? E.g. it does not matter that on November 2nd 2014, between 00:10 and 23:50, the UTC offset has changed (EDT->ET) and the two instants are separated by more than 24 hrs of time: new DateTimeOffset(2014, 11, 02, 00, 10, 00, new TimeSpan(-4, 0, 0)).Date == new DateTimeOffset(2014, 11, 02, 23, 50, 00, new TimeSpan(-5, 0, 0)).Date. Basically, this is what martijn tries to do, but in a very complicated way. When you would try just

public static bool SameDateSimple(DateTimeOffset first, DateTimeOffset second)
{
    return first.Date == second.Date;
}

it would work for all your abovementioned unit tests. And, also, this is what most humans would call “the same calendar day” when it is guaranteed the two instances refer to times at a single place.

Or, if you are really comparing two “random” timezones, you have to choose your reference timezone. It could be UTC as you tried initially. Or, it might be more logical from a human standpoint to use the first timezone as the reference (you could also choose the second one, it would give different results, but both variants are “equally good”):

public static bool SameDateGeneral(DateTimeOffset first, DateTimeOffset second)
{
    DateTime secondAdjusted = second.ToOffset(first.Offset).Date;
    return first.Date == secondAdjusted.Date;
}

This does not work for some of the abovementioned tests, but is more general in the sense it works “correctly” (in some sense) for two random timezones: If you try first = new DateTimeOffset(2014, 1, 2, 0, 30, 0, new TimeSpan(5, 0, 0)), second = new DateTimeOffset(2014, 1, 1, 23, 30, 0, new TimeSpan(4, 0, 0)), the simple SameDateSimple returns false (as does martijn’s), even though these two instances refer to the exact same moment in time (both are 2014-01-01 19:30:00Z). SameDateGeneral returns true here correctly.