将 Windows 时区转换为 moment.js 时区?
Convert Windows timezone to moment.js timezone?
我们在 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。
有没有办法严格地将 map/convert .NET TimeZoneInfo
转换为 Moment.js 时区对象?
更新
Jon 建议您必须在 momentjs 和 .NET 中使用 NodaTime BCL 或 IANA 数据。否则你会得到差异。我应该同意这一点。
您无法使用 TimeZoneInfo 在 .NET 4.5 中 100% 可靠地转换时间。即使您按照建议使用 NodaTime
或如下所示使用 TimeZoneToMomentConverter
进行转换。
原始答案
IANA 和 Windows 时区数据随时间更新并具有不同的粒度。
因此,如果您想要在 .NET 和 moment.js 中进行完全相同的转换 - 您必须
- 到处使用 IANA(按照 Matt 的建议使用 NodaTime),
- 到处使用 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
{
/// <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));
}
}
}
您也可以通过 NuGet 获取:
PM> Install-Package Pranas.WindowsTimeZoneToMomentJs
以及 GitHub 上代码和测试的浏览器源代码。
长话短说:
- 服务器端继续使用Noda Time
- 选择使用BCL数据还是IANA数据;我个人会推荐 IANA,但这是你的决定。 (撇开其他不说,IANA 数据的版本化更加清晰。)
- 使用 Noda Time 生成 moment.js 数据,这样您就可以确切地 知道客户端将使用什么,并且与您在服务器上所做的一致
- 针对数据变化时发生的情况制定策略
详情:
And sometimes need to convert TZ exactly on server-side (C#) and client-side (JS).
你需要得到完全两边相同的时区数据和两边的等效实现。这有问题,因为:
- IANA 时区数据会定期更新(例如,您需要能够说出 "use data 2015a")
- Windows时区数据定期更新
- 我不敢打赌 IANA 规则的每个实施都完全相同,即使它们应该是
- 我知道
TimeZoneInfo
实现随着时间的推移发生了变化,部分是为了删除一些 odd bugs and partly to include more data。 (.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 决定对 [=32] 的时区使用 positive 偏移量=]落后 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);
}
}
我们在 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。
有没有办法严格地将 map/convert .NET TimeZoneInfo
转换为 Moment.js 时区对象?
更新
Jon 建议您必须在 momentjs 和 .NET 中使用 NodaTime BCL 或 IANA 数据。否则你会得到差异。我应该同意这一点。
您无法使用 TimeZoneInfo 在 .NET 4.5 中 100% 可靠地转换时间。即使您按照建议使用 NodaTime
或如下所示使用 TimeZoneToMomentConverter
进行转换。
原始答案
IANA 和 Windows 时区数据随时间更新并具有不同的粒度。
因此,如果您想要在 .NET 和 moment.js 中进行完全相同的转换 - 您必须
- 到处使用 IANA(按照 Matt 的建议使用 NodaTime),
- 到处使用 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
{
/// <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));
}
}
}
您也可以通过 NuGet 获取:
PM> Install-Package Pranas.WindowsTimeZoneToMomentJs
以及 GitHub 上代码和测试的浏览器源代码。
长话短说:
- 服务器端继续使用Noda Time
- 选择使用BCL数据还是IANA数据;我个人会推荐 IANA,但这是你的决定。 (撇开其他不说,IANA 数据的版本化更加清晰。)
- 使用 Noda Time 生成 moment.js 数据,这样您就可以确切地 知道客户端将使用什么,并且与您在服务器上所做的一致
- 针对数据变化时发生的情况制定策略
详情:
And sometimes need to convert TZ exactly on server-side (C#) and client-side (JS).
你需要得到完全两边相同的时区数据和两边的等效实现。这有问题,因为:
- IANA 时区数据会定期更新(例如,您需要能够说出 "use data 2015a")
- Windows时区数据定期更新
- 我不敢打赌 IANA 规则的每个实施都完全相同,即使它们应该是
- 我知道
TimeZoneInfo
实现随着时间的推移发生了变化,部分是为了删除一些 odd bugs and partly to include more data。 (.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 决定对 [=32] 的时区使用 positive 偏移量=]落后 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);
}
}