将Windows时区转换为moment.js时区?
我们在ASP.NET中有一个应用程序,它以Windows格式存储所有用户时区数据(通过TimeZoneInfo.Id)。
我们还使用moment.js和moment.js TimeZone库将UTC数据转换为客户端的用户数据。 这是一个复杂的AngularJs应用程序,需要在客户端进行时区转换。
到目前为止,我们使用NodaTime .NET库将Windows时区ID转换为Moment.js时区ID。 它适用于大多数常见时区。 但我们需要使这种转换100%兼容。
目前看来,没有可靠的方法将Windows时区ID映射到IANA时区数据。 存在很多差异。
我相信现代JS应用经常处理时区。 有时需要在服务器端(C#)和客户端(JS)上完全转换TZ。
有没有办法严格地将.NET TimeZoneInfo
映射/转换为Moment.js时区对象?
TL; DR:
- 继续在服务器端使用Noda Time
- 选择是否使用BCL数据或IANA数据; 我个人推荐IANA,但这是你的电话。 (除此之外,IANA数据的版本更清晰。)
- 使用Noda Time生成moment.js数据,以便准确了解客户端将使用的内容,并且它与您在服务器上执行的操作一致
- 针对数据发生变化时的情况制定策略
细节:
有时需要在服务器端(C#)和客户端(JS)上完全转换TZ。
您需要在两侧获得完全相同的时区数据,并在两侧获得等效的实现。 这有问题,因为:
- IANA时区数据定期更新(因此您需要能够说“使用数据2015a”)
- Windows时区数据会定期更新
- 我不想打赌,IANA规则的每个实施都是完全相同的,即使它们应该是
- 我知道
TimeZoneInfo
实现随着时间的推移而发生了变化,部分原因是为了消除一些奇怪的错误 ,部分是为了包含更多的数据 。 (.NET 4.6理解时区的概念在历史记录中更改其标准偏移量;早期版本不会
使用Noda Time,您可以非常轻松地将BCL或IANA时区数据转换为moment.js格式 – 并且比Evgenyt的代码更可靠地执行此操作,因为TimeZoneInfo
不允许您请求转换。 (由于TimeZoneInfo
本身的错误,有一些小口袋,偏移量可以改变几个小时 – 他们不应该,但如果你想要完全匹配TimeZoneInfo
行为,你需要能够找到所有这些 – Evgenyt的代码并不总能发现那些。)即使Noda Time没有完全反映TimeZoneInfo
,它也应该与自己一致。
moment.js格式看起来很简单,所以只要你不介意将数据发送到客户端,这绝对是一个选择。 您需要考虑数据更改时要执行的操作:
- 你怎么在服务器上拿起它?
- 您如何使用旧数据临时处理客户?
如果确切的一致性对您来说非常重要,您可能希望将时区数据发送到具有时区数据版本的客户端…然后客户端可以在发布数据时将其呈现给服务器。 (当然,我假设它正在这样做。)然后服务器可以使用该版本,或者拒绝客户端的请求,并说有更新的数据。
这里有一些示例代码将Noda时区数据转换为moment.js – 它看起来对我来说没问题,但我没有做太多。 它与momentjs.com中的文档相匹配…请注意,偏移量必须反转,因为由于某种原因,moment.js决定对UTC 后面的时区使用正偏移量。
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建议您必须在momentjs和.NET中使用NodaTime BCL或IANA数据。 否则你会得到不符点。 我应该同意这一点。
您不能使用TimeZoneInfo 100%可靠地在.NET 4.5中转换时间。 即使你按照建议使用NodaTime
转换它,或者如下所示使用TimeZoneToMomentConverter
转换它。
原始答案
IANA和Windows时区数据随时间更新并具有不同的粒度。
因此,如果你想在.NET和moment.js中完全相同的转换 – 你要么
- 在任何地方使用IANA(使用NodaTime,如Matt所建议的),
- 到处使用Windows时区(将TimeZoneInfo规则转换为moment.js格式)。
我们采取了第二种方式,并实施了转换器。
它增加了线程安全缓存以提高效率,因为它基本上循环遍历日期(而不是尝试自己转换TimeZoneInfo
规则)。 在我们的测试中,它以100%的准确度转换当前的Windows时区(请参阅GitHub上的测试)。
这是该工具的代码:
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)); } } }
你也可以通过NuGet获得它:
PM> Install-Package Pranas.WindowsTimeZoneToMomentJs
和GitHub上的代码和测试的浏览器源代码。