UPDATE
John suggested that you need to use NodaTime BCL or IANA data in both moments and .NET. Otherwise, you will receive discrepancies. I have to agree with that.
You cannot 100% reliably convert time to .NET 4.5 using TimeZoneInfo. Even if you convert it using NodaTime , as suggested, or TimeZoneToMomentConverter , as shown below.
ORIGINAL RESPONSE
Updating IANA and Windows time data over time and varying degrees of granularity.
So, if you want the exact same conversion in .NET and moment.js - you have either
- use IANA everywhere (with NodaTime as Matt suggested)
- use the Windows time zone all over the world (convert TimeZoneInfo rules to moment.js format).
We went the second way and implemented the converter.
It adds a thread-safe cache to be more efficient, as it basically loops through dates (instead of trying to convert the TimeZoneInfo rules themselves). In our tests, it converts the current Windows time zones with 100% accuracy (see GitHub Tests).
This is the tool code:
using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web.Script.Serialization; namespace Pranas.WindowsTimeZoneToMomentJs { /// <summary> /// Tool to generates JavaScript that adds MomentJs timezone into moment.tz store. /// As per http://momentjs.com/timezone/docs/ /// </summary> public static class TimeZoneToMomentConverter { private static readonly DateTimeOffset UnixEpoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero); private static readonly JavaScriptSerializer Serializer = new JavaScriptSerializer(); private static readonly ConcurrentDictionary<Tuple<string, int, int, string>, string> Cache = new ConcurrentDictionary<Tuple<string, int, int, string>, string>(); /// <summary> /// Generates JavaScript that adds MomentJs timezone into moment.tz store. /// It caches the result by TimeZoneInfo.Id /// </summary> /// <param name="tz">TimeZone</param> /// <param name="yearFrom">Minimum year</param> /// <param name="yearTo">Maximum year (inclusive)</param> /// <param name="overrideName">Name of the generated MomentJs Zone; TimeZoneInfo.Id by default</param> /// <returns>JavaScript</returns> public static string GenerateAddMomentZoneScript(TimeZoneInfo tz, int yearFrom, int yearTo, string overrideName = null) { var key = new Tuple<string, int, int, string>(tz.Id, yearFrom, yearTo, overrideName); return Cache.GetOrAdd(key, x => { var untils = EnumerateUntils(tz, yearFrom, yearTo).ToArray(); return string.Format( @"(function(){{ var z = new moment.tz.Zone(); z.name = {0}; z.abbrs = {1}; z.untils = {2}; z.offsets = {3}; moment.tz._zones[z.name.toLowerCase().replace(/\//g, '_')] = z; }})();", Serializer.Serialize(overrideName ?? tz.Id), Serializer.Serialize(untils.Select(u => "-")), Serializer.Serialize(untils.Select(u => u.Item1)), Serializer.Serialize(untils.Select(u => u.Item2))); }); } private static IEnumerable<Tuple<long, int>> EnumerateUntils(TimeZoneInfo timeZone, int yearFrom, int yearTo) { // return until-offset pairs int maxStep = (int)TimeSpan.FromDays(7).TotalMinutes; Func<DateTimeOffset, int> offset = t => (int)TimeZoneInfo.ConvertTime(t, timeZone).Offset.TotalMinutes; var t1 = new DateTimeOffset(yearFrom, 1, 1, 0, 0, 0, TimeSpan.Zero); while (t1.Year <= yearTo) { int step = maxStep; var t2 = t1.AddMinutes(step); while (offset(t1) != offset(t2) && step > 1) { step = step / 2; t2 = t1.AddMinutes(step); } if (step == 1 && offset(t1) != offset(t2)) { yield return new Tuple<long, int>((long)(t2 - UnixEpoch).TotalMilliseconds, -offset(t1)); } t1 = t2; } yield return new Tuple<long, int>((long)(t1 - UnixEpoch).TotalMilliseconds, -offset(t1)); } } }
You can also get it through NuGet:
PM> Install-Package Pranas.WindowsTimeZoneToMomentJs
And browser sources for code and tests on GitHub .