Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using NodaTime to convert invalid (skipped) datetime values to UTC

What I want to accomplish is to convert a DateTime (parsed from a string assumed to be in EST/EDT) to UTC. I am using NodaTime because I need to use Olson timezones.

Converting an invalid (skipped) DateTime to UTC using NodaTime's ZoneLocalMappingResolver is not converting minute and seconds part of the input because I have configured the CustomResolver to return the start of interval after the gap. NodaTime does not seem to have an equivalent of TimeZoneInfo.IsInvalidTime.

How do I use NodaTime to convert skipped datetime values to UTC and match the result of GetUtc() method in the Utils class below? (Utils.GetUtc method uses System.TimeZoneInfo not NodaTime)

This is the test case :

[TestMethod]
public void Test_Invalid_Date()
{
    var ts = new DateTime(2013, 3, 10, 2, 15, 45);

    // Convert to UTC using System.TimeZoneInfo
    var utc = Utils.GetUtc(ts).ToString(Utils.Format);

    // Convert to UTC using NodaTime (Tzdb/Olson dataabase)
    var utcNodaTime = Utils.GetUtcTz(ts).ToString(Utils.Format);

    Assert.AreEqual(utc, utcNodaTime);
}

This is what I am getting :

Assert.AreEqual failed. Expected:<2013-03-10 07:15:45.000000>. Actual:<2013-03-10 07:00:00.000000>.

Here is the Utils class (also on github):

using System;

using NodaTime;
using NodaTime.TimeZones;

/// <summary>
/// Functions to Convert To and From UTC
/// </summary>
public class Utils
{
    /// <summary>
    /// The date format for display/compare
    /// </summary>
    public const string Format = "yyyy-MM-dd HH:mm:ss.ffffff";

    /// <summary>
    /// The eastern U.S. time zone
    /// </summary>
    private static readonly NodaTime.DateTimeZone BclEast = NodaTime.DateTimeZoneProviders.Bcl.GetZoneOrNull("Eastern Standard Time");


    private static readonly TimeZoneInfo EasternTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");

    private static readonly NodaTime.DateTimeZone TzEast = NodaTime.DateTimeZoneProviders.Tzdb.GetZoneOrNull("America/New_York");

    private static readonly ZoneLocalMappingResolver CustomResolver = Resolvers.CreateMappingResolver(Resolvers.ReturnLater, Resolvers.ReturnStartOfIntervalAfter);

    public static DateTime GetUtc(DateTime ts)
    {
        return TimeZoneInfo.ConvertTimeToUtc(EasternTimeZone.IsInvalidTime(ts) ? ts.AddHours(1.0) : ts, EasternTimeZone);
    }

    public static DateTime GetUtcTz(DateTime ts)
    {
        var local = LocalDateTime.FromDateTime(ts);
        var zdt = TzEast.ResolveLocal(local, CustomResolver);
        return zdt.ToDateTimeUtc();            
    }

    public static DateTime GetUtcBcl(DateTime ts)
    {
        var local = LocalDateTime.FromDateTime(ts);
        var zdt = BclEast.ResolveLocal(local, CustomResolver);
        return zdt.ToDateTimeUtc();
    }
}
like image 743
ash Avatar asked Mar 04 '13 23:03

ash


1 Answers

NodaTime does not seem to have an equivalent of TimeZoneInfo.IsInvalidTime.

Well, rather than just asking that one question - and then having to ask follow-on ones - you use DateTimeZone.MapLocal. That gives you everything you could know about a local to UTC mapping: whether it's unambiguous, ambiguous, or invalid.

Alternatively, use ResolveLocal but with your own custom SkippedTimeResolver delegate.

For example, making this change makes your code work for me:

private static readonly ZoneLocalMappingResolver CustomResolver = 
    Resolvers.CreateMappingResolver(Resolvers.ReturnLater, AddGap);

// SkippedTimeResolver which adds the length of the gap to the
// local date and time.
private static ZonedDateTime AddGap(LocalDateTime localDateTime,
                                    DateTimeZone zone,
                                    ZoneInterval intervalBefore,
                                    ZoneInterval intervalAfter)        
{
    long afterMillis = intervalAfter.WallOffset.Milliseconds;
    long beforeMillis = intervalBefore.WallOffset.Milliseconds;
    Period gap = Period.FromMilliseconds(afterMillis - beforeMillis);
    return zone.AtStrictly(localDateTime + gap);
}

(There are other equivalent ways of doing it, of course.)

I'd personally suggest trying to avoid converting to and from DateTime unless you really have to - I would do as much as possible in Noda Time.

like image 73
Jon Skeet Avatar answered Oct 16 '22 09:10

Jon Skeet