将Windows时区转换为moment.js时区?

Rod*_*ion 7 javascript c# asp.net timezone momentjs

我们在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时区对象?

Jon*_*eet 7

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);
    }
}
Run Code Online (Sandbox Code Playgroud)


Evg*_*nyt 5

更新

Jon 建议您必须在 momentjs 和 .NET 中使用 NodaTime BCL 或 IANA 数据。否则你会得到差异。我应该同意这一点。

您无法使用 TimeZoneInfo 100% 可靠地转换 .NET 4.5 中的时间。即使您按照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));
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您还可以通过 NuGet 获取它:

PM> Install-Package Pranas.WindowsTimeZoneToMomentJs
Run Code Online (Sandbox Code Playgroud)

以及GitHub上代码和测试的浏览器源。