Convert Windows timezone to moment.js timezone?

后端 未结 2 1985
死守一世寂寞
死守一世寂寞 2021-02-04 16:16

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

2条回答
  •  -上瘾入骨i
    2021-02-04 16:53

    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

    • use IANA everywhere (with NodaTime as suggested Matt),
    • use Windows timezone everywhere (convert TimeZoneInfo rules to moment.js format).

    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.

提交回复
热议问题