Search code examples
c#datetimetimezoneportable-class-librarynodatime

Elegant way to convert epoch timestamp to Eastern Time and reverse using Nodatime


I am working on writing a managed wrapper around Massachusetts Bay Transportation Authority (MBTA) Realtime API. They have a API which returns the server time which is unix timestamp (epoch). The library under which I am implementing it is PCL Profile 78 which means I have limited support for BCL TimeZone, so I resorted to using Nodatime

I am trying to convert the time returned from server to Eastern Time which is America/New_York as a DateTime object and reverse way. My current code is very dirty

public static class TimeUtils
{
    static readonly DateTimeZone mbtaTimeZone = DateTimeZoneProviders.Tzdb["America/New_York"];
    static readonly DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

    public static DateTime GetMbtaDateTime (long unixTimestamp)
    {
        var mbtaEpochTime = epoch.AddSeconds (unixTimestamp);
        var instant = Instant.FromUtc (mbtaEpochTime.Year, mbtaEpochTime.Month,
            mbtaEpochTime.Day, mbtaEpochTime.Hour, mbtaEpochTime.Minute, mbtaEpochTime.Second);
        var nodaTime = instant.InZone (mbtaTimeZone);
        return nodaTime.ToDateTimeUnspecified ();
    }

    public static long MbtaDateTimeToUnixTimestamp (DateTime time)
    {
        TimeSpan secondsSinceEpochMbtaTz = time - epoch;
        var instant = Instant.FromUtc (time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second);
        var mbtaTzSpan = mbtaTimeZone.GetUtcOffset (instant).ToTimeSpan ();
        var epochDiff = secondsSinceEpochMbtaTz - mbtaTzSpan;
        return (long)epochDiff.TotalSeconds;
    }
}

Is there another way to write this simply. I hope Nodatime should have support for converting an epoch time to America/New_York DateTime and America/New_York DateTime to epoch time. My method MbtaDateTimeToUnixTimestamp is a brutal hack


Solution

  • Firstly, as mentioned in comments, it would be best to use Noda Time types throughout your code - only resort to DateTime when you really have to. This should lead to significantly cleaner code throughout.

    Converting a Unix timestamp to an Instant is really easy:

    Instant instant = Instant.FromUnixTimeSeconds(seconds);
    

    You can then convert into a ZonedDateTime as per your current code... and using ToDateTimeUnspecified is fine if you really need to use DateTime.

    For the reverse, your current code looks broken to me - you're assuming the DateTime is a UTC value, effectively. That would be at odds with your later use of the time zone. I suspect you want to convert the input to a LocalDateTime, and then apply the time zone. For example:

    public static long MbtaDateTimeToUnixTimestamp(DateTime time)
    {
        var local = LocalDateTime.FromDateTime(time);
        var zoned = local.InZoneStrictly(mbtaTimeZone);
        var instant = zoned.ToInstant();
        return instant.Ticks / NodaConstants.TicksPerSecond;
    }
    

    Note the InZoneStrictly call. This will throw an exception if either you pass in a local time which didn't exist or one that existed twice - in both cases due to DST transitions. This may well not be what you want - you really need to think about what you want to happen in those cases, or try to avoid them being feasible. See the time zones section of the documentation for more details and options.