Converting between time zones with Noda Time

PureCognition picture PureCognition · Oct 8, 2013 · Viewed 16.9k times · Source

I'm currently trying to ensure that our legacy back-end can support resolving date times based on the user's current time zone (or, more specifically offset). Our servers are in eastern standard time, and most of our date times originate there. However, for users that are in other time zones, a conversion to their time zone (or, in this case, offset) is needed when retrieving those date times. Also, date times coming from the user will have to be converted to eastern standard time before persistence on the server. Given that the front end we are developing is web-based, I am able to retrieve the user's offset in minutes and pass that value into my service layer within the header. I looked at Noda Time and think it's a great API. It did force me to think about time in a more refined matter, but I am still not 100% sure that I've properly used it correctly. Here are the methods that I wrote for the conversions described above. I've tested them and they seem to work. Given the scenario above, does this look like a proper use of the library? Am I thinking about date times properly?

public static DateTime ConvertToUtcFromEasternTimeZone(DateTime easternDateTime)
{
    NodaTime.DateTimeZone easternTimeZone = NodaTime.DateTimeZoneProviders.Tzdb.GetZoneOrNull("America/New_York");
    ZoneLocalMappingResolver customResolver = Resolvers.CreateMappingResolver(Resolvers.ReturnLater, Resolvers.ReturnStartOfIntervalAfter);
    var easternLocalDateTime = LocalDateTime.FromDateTime(easternDateTime);
    var easternZonedDateTime = easternTimeZone.ResolveLocal(easternLocalDateTime, customResolver);
    return easternZonedDateTime.ToDateTimeUtc();
}

public static DateTime ConvertToEasternTimeZoneFromUtc(DateTime utcDateTime)
{
    NodaTime.DateTimeZone easternTimeZone = NodaTime.DateTimeZoneProviders.Tzdb.GetZoneOrNull("America/New_York");
    NodaTime.DateTimeZone utcTimeZone = NodaTime.DateTimeZoneProviders.Tzdb.GetZoneOrNull("UTC");
    ZoneLocalMappingResolver customResolver = Resolvers.CreateMappingResolver(Resolvers.ReturnLater, Resolvers.ReturnStartOfIntervalAfter);
    var utcLocal = LocalDateTime.FromDateTime(utcDateTime);
    var utcZonedDateTime = utcTimeZone.ResolveLocal(utcLocal, customResolver);
    var easternZonedDateTime = utcZonedDateTime.ToInstant().InZone(easternTimeZone);
    return easternZonedDateTime.ToDateTimeUnspecified();
}

public static DateTime ConvertToUtc(DateTime dateTime, int offsetInMinutes)
{
    LocalDateTime localDateTime = LocalDateTime.FromDateTime(dateTime);
    var convertedDateTime = localDateTime.PlusMinutes(offsetInMinutes).ToDateTimeUnspecified();
    return convertedDateTime;
}

public static DateTime ConvertFromUtc(DateTime dateTime, int offsetInMinutes)
{
    LocalDateTime localDateTime = LocalDateTime.FromDateTime(dateTime);
    var convertedDateTime = localDateTime.PlusMinutes(-offsetInMinutes).ToDateTimeUnspecified();
    return convertedDateTime;
}

The idea here is that time zone matters when I'm resolving between UTC time and the time zone in the database. When I'm resolving between the client time and UTC time then offset matters.

In the future, we can persist UTC time, and this will be easier. Currently, this solution is a stop gap.

The idea is that we are going to go from...

client -> UTC +/- offset -> UTC -> Eastern Time -> database

database -> Eastern Time -> UTC -> UTC +/- offset -> client

to eventually...

client -> UTC +/- offset -> UTC -> database

database -> UTC -> UTC +/- offset -> client

Answer

Jon Skeet picture Jon Skeet · Oct 8, 2013

Your first method looks okay, although we don't know what customResolver is.

Your second method is a bit off. I'd suggest:

public static DateTime ConvertToEasternTimeZoneFromUtc(DateTime utcDateTime)
{
    var easternTimeZone = DateTimeZoneProviders.Tzdb["America/New_York"];
    return Instant.FromDateTimeUtc(utcDateTime)
                  .InZone(easternTimeZone)
                  .ToDateTimeUnspecified();
}

Note that you don't need to look up the Eastern time zone in every method call - just have:

private static readonly DateTimeZone EasternTimeZone = 
    DateTimeZoneProviders.Tzdb["America/New_York"];

... then use that everywhere.

Your third and fourth methods aren't what I'd think of as idiomatic - for the third method you should use:

public static DateTime ConvertToUtc(DateTime dateTime, int offsetInMinutes)
{
    var offset = Offset.FromMinutes(offsetInMinutes);
    var localDateTime = LocalDateTime.FromDateTime(dateTime);
    return new OffsetDateTime(localDateTime, offset).ToInstant()
                                                    .ToDateTimeUtc();
}

The fourth method seems a bit trickier, as we don't provide everything we should in terms of conversions with OffsetDateTime. The code you've used is probably okay, but it would certainly be cleaner if you could use OffsetDateTime.

EDIT: I've now added a method to Instant to make the fourth method cleaner. It will be part of 1.2.0, and you can use:

public static DateTime ConvertFromUtc(DateTime dateTime, int offsetInMinutes)
{
    var offset = Offset.FromMinutes(offsetInMinutes);
    var instant = Instant.FromDateTimeUtc(dateTime);
    return instant.WithOffset(offset)
                  .LocalDateTime
                  .ToDateTimeUnspecified();
}