Convert Windows timezone to moment.js timezone?

We have an app in ASP.NET that stores all user timezone data in Windows format (by TimeZoneInfo.Id).

We also use moment.js and moment.js TimeZone libraries to convert UTC data to user data on client side. It is a complex AngularJs application that needs to be do timezone conversion on client side.

So far we used NodaTime .NET library to convert Windows timezone ID to Moment.js timezone ID. It worked well for most common time zones. But we need to make this conversion 100% compatible.

Currently it appears, that there's no reliable way to map Windows timezone ID to IANA Time Zone data. There are a lot of discrepancies.

I believe modern JS apps deal with time zones often. And sometimes need to convert TZ exactly on server-side (C#) and client-side (JS).

Is there a way to strictly map/convert .NET TimeZoneInfo into Moment.js timezone object?

Jon Skeet
people
quotationmark

TL;DR:

  • Keep using Noda Time on the server side
  • Choose whether to use BCL data or IANA data; I would personally recommend IANA, but it's your call. (Aside from anything else, IANA data is more clearly versioned.)
  • Use Noda Time to generate moment.js data so you know exactly what the client will use, and that it'll be consistent with what you do on the server
  • Work out a strategy for what happens when the data changes

Details:

And sometimes need to convert TZ exactly on server-side (C#) and client-side (JS).

You need to get exactly the same time zone data on both sides and equivalent implementations on both sides. This has problems because:

  • IANA time zone data is updated regularly (so you'd need to be able to say "use data 2015a" for example)
  • Windows time zone data is updated regularly
  • I wouldn't like to bet that every implementation of IANA rules is exactly the same, even though they should be
  • I know that the TimeZoneInfo implementation has changed over time, partly to remove some odd bugs and partly to include more data. (.NET 4.6 understands the concept of a time zone changing its standard offset over history; earlier versions don't)

With Noda Time you could pretty easily convert either BCL or IANA time zone data to moment.js format - and do so more reliably than Evgenyt's code, because TimeZoneInfo doesn't allow you to request transitions. (Due to bugs in TimeZoneInfo itself, there are small pockets where offsets can change just for a few hours - they shouldn't, but if you want to match TimeZoneInfo behaviour exactly, you'd need to be able to find all of those - Evgenyt's code won't always spot those.) Even if Noda Time doesn't mirror TimeZoneInfo exactly, it should be consistent with itself.

The moment.js format looks pretty simple, so as long as you don't mind shipping the data to the client, that's definitely an option. You need to think about what to do when the data changes though:

  • How do you pick it up on the server?
  • How do you cope with the client temporarily using old data?

If exact consistency is really important to you, you may well want to ship the time zone data to the client with a time zone data version... which the client can then present back to the server when it posts data. (I'm assuming it's doing so, of course.) The server could then either use that version, or reject the client's request and say there's more recent data.

Here's some sample code to convert Noda Time zone data into moment.js - it looks okay to me, but I haven't done much with it. It matches the documentation in momentjs.com... note that the offset has to be reversed because moment.js decides to use positive offsets for time zones which are behind UTC, for some reason.

using System;
using System.Linq;

using NodaTime;
using Newtonsoft.Json;

class Test
{
    static void Main(string[] args)
    {
        Console.WriteLine(GenerateMomentJsZoneData("Europe/London", 2010, 2020));
    }

    static string GenerateMomentJsZoneData(string tzdbId, int fromYear, int toYear)
    {
        var intervals = DateTimeZoneProviders
            .Tzdb[tzdbId]
            .GetZoneIntervals(Instant.FromUtc(fromYear, 1, 1, 0, 0),
                              Instant.FromUtc(toYear + 1, 1, 1, 0, 0))
            .ToList();

        var abbrs = intervals.Select(interval => interval.Name);
        var untils = intervals.Select(interval => interval.End.Ticks / NodaConstants.TicksPerMillisecond);
        var offsets = intervals.Select(interval => -interval.WallOffset.Ticks / NodaConstants.TicksPerMinute);
        var result = new { name = tzdbId, abbrs, untils, offsets };
        return JsonConvert.SerializeObject(result);
    }
}

people

See more on this question at Stackoverflow