计算 DST 转换边界处的持续时间(向前/后退的数量)

Posted

技术标签:

【中文标题】计算 DST 转换边界处的持续时间(向前/后退的数量)【英文标题】:Calculate duration (amount to spring forward/fall back) at DST transitions boundaries 【发布时间】:2021-03-13 17:35:35 【问题描述】:

从这个很好的答案中,我能够确定夏令时转换日期: https://***.com/a/24378695/1159939

除了这些日期之外,我还需要知道时钟会增加还是减少以及增加或减少多少(总是一小时?)。

例如,在美国/太平洋时区,当本地时钟达到 2020-03-08T02:00:00 时,我需要以某种方式获取值 +1h。当时钟到达 2020-11-01T02:00:00 时,我需要获取值 -1h。

在 NodaTime 中,有一个 Offset.Savings 值为 +0 或 +1。现在确定我可以如何使用它。

--更新1: 我正在构建一个调度程序。我需要让用户更好地控制在 1 小时的节省期内发生计划作业时作业的执行方式。

我正在考虑让用户可以选择以下设置:

[x] 时钟快进时运行错过的作业。

[x] 时钟倒退时重新运行作业。

例如,假设某个作业计划在 2020-03-08T02:15:00 US/Pacific 运行。这个当地时间不存在。如果用户勾选“时钟快进时运行错过的作业”复选框,该作业将在凌晨 3:15 执行,否则,该作业将被跳过。

例如,假设某个作业计划在 2020-11-01T01:45:00 US/Pacific 运行。这个当地时间将出现两次。如果用户勾选了“时钟回退时重新运行作业”,则该作业将执行两次,否则将执行一次。

为了进行上述计算,我需要知道我从前面提到的帖子中得到的夏令时转换日期。我还需要知道时钟的变化方向和变化幅度(例如:1 小时)。

--更新2:

经过深思熟虑,我认为我需要一个包含以下数据的时区转换列表:

2020-03-08 02:00:00 US/Eastern | 2020-03-08 07:00:00 UAT (01:00:00)
2020-11-01 02:00:00 US/Eastern | 2020-11-01 06:00:00 UAT (-01:00:00)

下面是我用来生成这些数据的代码。不确定这是否适用于所有情况。为了计算时间变化,我使用了下一个区域间隔的开始和当前区域间隔的结束之间的差异。

using NodaTime;
using System;
using System.Collections.Generic;

namespace ConsoleApp

    class Program
    
        static void Main(string[] args)
        
            string timeZoneId = "US/Eastern";
            DateTimeZone? timeZone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(timeZoneId);

            if (timeZone is null)
                throw new Exception($"Cannot find time zone 'timeZoneId'.");

            int year = 2020;

            var daylightSavingTransitions = GetDaylightSavingTransitions(timeZone, year);

            foreach (var daylightSavingTransition in daylightSavingTransitions)
            
                Console.WriteLine(daylightSavingTransition);
            


            /// <summary>
            /// Get points in time when a daylight saving time transitions occur.
            /// </summary>
            /// <param name="timeZone">Time zone of the local clock.</param>
            /// <param name="year">The year to find transitions.</param>
            /// <returns></returns>
            static IEnumerable<DaylightSavingTransition> GetDaylightSavingTransitions(DateTimeZone timeZone, int year)
            
                var yearStart = new LocalDateTime(year, 1, 1, 0, 0).InZoneLeniently(timeZone).ToInstant();
                var yearEnd = new LocalDateTime(year + 1, 1, 1, 0, 0).InZoneLeniently(timeZone).ToInstant();

                LinkedList<NodaTime.TimeZones.ZoneInterval> zoneIntervals = new LinkedList<NodaTime.TimeZones.ZoneInterval>(timeZone.GetZoneIntervals(yearStart, yearEnd));
                LinkedListNode<NodaTime.TimeZones.ZoneInterval>? currentNode = zoneIntervals.First;

                while (currentNode is  )
                
                    if (currentNode.Next is null)
                        break;

                    //Time change is the difference between the start of the next zone interval and the end of the current zone interval.
                    Period timeChangePeriod = currentNode.Next.Value.IsoLocalStart - currentNode.Value.IsoLocalEnd;
                    TimeSpan timeChange = new TimeSpan(Convert.ToInt32(timeChangePeriod.Hours), Convert.ToInt32(timeChangePeriod.Minutes), Convert.ToInt32(timeChangePeriod.Seconds));
                    DaylightSavingTransition daylightSavingTransition = new DaylightSavingTransition(timeZone.Id, currentNode.Value.IsoLocalEnd.ToDateTimeUnspecified(), currentNode.Value.End.ToDateTimeUtc(), timeChange);
                    yield return daylightSavingTransition;

                    currentNode = currentNode.Next;
                
            
        
    



    public class DaylightSavingTransition
    
        public DaylightSavingTransition(string timeZoneId, DateTime transitionLocalDate, DateTime transitionUtcDate, TimeSpan timeChange)
        
            TimeZoneId = timeZoneId;
            TransitionLocalDate = DateTime.SpecifyKind(transitionLocalDate, DateTimeKind.Unspecified);
            TransitionUtcDate = DateTime.SpecifyKind(transitionUtcDate, DateTimeKind.Utc);
            TimeChange = timeChange;
        

        public string TimeZoneId  get; 
        public DateTime TransitionLocalDate  get; 
        public DateTime TransitionUtcDate  get; 
        public TimeSpan TimeChange  get; 

        /// <summary>
        /// For fall back transition, used to determine if date is in the duplicated time period.
        /// </summary>
        /// <param name="utcDateTime">Point in time to test if it is inside the repeating time period.</param>
        public bool IsRepeatingDateTime(DateTime utcDateTime)
        
            if (utcDateTime >= TransitionUtcDate && utcDateTime < TransitionUtcDate.Add(TimeChange.Duration()))
            
                return true;
            
            else
            
                return false;
            
        

        public override string ToString()
        
            return $"TransitionLocalDate.ToString("yyyy-MM-dd HH:mm:ss") TimeZoneId | TransitionUtcDate.ToString("yyyy-MM-dd HH:mm:ss") UAT (TimeChange)";
        
    

【问题讨论】:

不清楚您所说的“时钟到达时”是什么意思。您要编写的代码的 shape 是什么? (如果它是一种方法,那么该方法签名会是什么样的?)我怀疑您对 ZoneIntervalZoneLocalMapping 类型感兴趣,但是如果不知道您是什么,我无法在代码中给出具体示例期待。 @JonSkeet 谢谢乔恩。我已经用更多细节更新了主帖。 【参考方案1】:

听起来您真正在寻找的是DateTimeZone.MapLocal(LocalDateTime)。这将返回一个ZoneLocalMapping,它告诉您本地日期/时间如何映射到指定的时区。重要的属性是:

Count 0 如果日期/时间被跳过 如果映射明确,则为 1 2 如果映射不明确 EarlyIntervalLateInterval 是围绕指定 LocalDateTime 的任何转换之前/之后的 ZoneInterval 值(如果值不在转换中,则为相同的 ZoneInterval

ZoneInterval 包含一个 WallOffset,它是该区域间隔期间的总体 UTC 偏移量。我强烈建议使用它而不是Savings,以应对不是夏令时转换的转换(例如,如果区域的标准时间发生变化)。

您应该能够使用该信息来确定何时运行。

您还可以使用 DateTimeZone.ResolveLocal(LocalDateTime, ZoneLocalMappingResolver) 根据用户选择的内容构建自己的解析器(用于处理跳过/不明确的时间)。

【讨论】:

谢谢@JonSkeet。我已经用我认为我需要的内容更新了我的原始帖子。但不确定我所做的是否适用于所有情况。如果您有时间,请告诉我它是否有效或是否有更好的解决方案。非常感谢您的宝贵时间,并感谢您在 Noda Time 上所做的工作。 @Sandy:您的DaylightSavingTransition 课程确实复制了ZoneInterval 中的很多信息。我希望使用ZoneInterval 会更简单。您应该使用两个 ZoneInterval 值之间的 WallOffset 属性差异来确定时钟变化的程度 - 这肯定比使用一段时间要好。如果您想保留您的 DaylightSavingTransition 类(由于夏令时存在不是的转换,因此命名不准确,顺便说一句)我建议您只提供两个 ZoneInterval 字段“之前”和“之后”。 @Sandy:然后您可以从中计算出您需要的属性。我建议尽可能坚持使用 Noda Time 类型,而不是公开 DateTime 值。 我重命名了类并重构了我的代码以包含 ZoneInterval 之前和之后的内容。我无法通过我的代码使用 Noda Time 类型,因为我需要将数据从 Noda Time 类型移动到我的 DTO 中的 DateTime 和 TimeSpan。我在 ZoneInterval 开始和结束时使用 ToDateTimeUtc()。我在 WallOffset 上使用 ToTimeSpan()。我不认为以下问题与使用 BCL 类型有关,但是当我现在计算本地时区结束日期时,开始 DST 期间我会提前 1 小时。例如,对于加拿大/东部,我得到 2020-03-08T03:00:00 而不是 2020-03-08T02:00:00。知道为什么会这样吗? @Sandy:我建议为您的 DTO 使用 Noda Time 类型 :) 无论如何,请使用 minimal reproducible example 就加拿大/东部方面提出一个新问题 - 我不想开始有效地尝试在评论线程中进行调试。

以上是关于计算 DST 转换边界处的持续时间(向前/后退的数量)的主要内容,如果未能解决你的问题,请参考以下文章

由于 DST 时钟向前,perl DateTime 和不存在的时间用户输入

计算机系统的数制及转换

服务边界处的 WCF 错误日志记录

如何使用 fullcalendar 处理 DST 显示混乱

在python中获取给定时区的DST边界

为啥 pytz 在跨越 TZ 和 DST 边界而不是 TZ 名称时正确调整时间和偏移量?