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
TL;DR:
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:
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:
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);
}
}
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
{
/// <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 via NuGet:
PM> Install-Package Pranas.WindowsTimeZoneToMomentJs
And browser sources for code and tests on GitHub.