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 UT
UPDATE
Jon suggested that you have to use NodaTime BCL or IANA data in both momentjs and .NET. Otherwise you'll get discrepancies. I should agree with this.
You cannot 100% reliably convert time in .NET 4.5 using TimeZoneInfo. Even if you convert it using NodaTime
as suggested, or TimeZoneToMomentConverter
as below.
ORIGINAL ANSWER
IANA and Windows timezone data update over time and have different granularity.
So if you want exactly the same conversion in .NET and moment.js - you have either to
We went the second way, and implemented the converter.
It adds thread-safe cache to be more efficient, as it basically loops through dates (instead of trying to convert TimeZoneInfo
rules themselves). In our tests it converts current Windows timezones with 100% accuracy (see tests on GitHub).
This is the code of the tool:
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
{
///
/// Tool to generates JavaScript that adds MomentJs timezone into moment.tz store.
/// As per http://momentjs.com/timezone/docs/
///
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, string> Cache = new ConcurrentDictionary, string>();
///
/// Generates JavaScript that adds MomentJs timezone into moment.tz store.
/// It caches the result by TimeZoneInfo.Id
///
/// TimeZone
/// Minimum year
/// Maximum year (inclusive)
/// Name of the generated MomentJs Zone; TimeZoneInfo.Id by default
/// JavaScript
public static string GenerateAddMomentZoneScript(TimeZoneInfo tz, int yearFrom, int yearTo, string overrideName = null)
{
var key = new Tuple(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> EnumerateUntils(TimeZoneInfo timeZone, int yearFrom, int yearTo)
{
// return until-offset pairs
int maxStep = (int)TimeSpan.FromDays(7).TotalMinutes;
Func 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)(t2 - UnixEpoch).TotalMilliseconds, -offset(t1));
}
t1 = t2;
}
yield return new Tuple((long)(t1 - UnixEpoch).TotalMilliseconds, -offset(t1));
}
}
}
You can also get it via NuGet:
PM> Install-Package Pranas.WindowsTimeZoneToMomentJs
And browser sources for code and tests on GitHub.