计算 DST 转换边界的持续时间(总计 spring forward/fall 返回)

Calculate duration (amount to spring forward/fall back) at DST transitions boundaries

从这个很好的答案中,我能够确定夏令时转换日期:

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

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

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

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

我正在考虑让用户可以进行以下设置 select:

[x] 运行 时钟向前 spring 时错过了工作。

[x] 在时钟回落时重新运行 工作。

例如,假设一项工作计划在 2020-03-08T02:15:00 运行 US/Pacific。这个当地时间不存在。如果用户勾选“运行 missed jobs when clocks spring forward”复选框,作业将在 3:15AM 处执行,否则作业将被跳过。

例如,假设一项工作被安排在 运行 在 2020-11-01T01:45:00 US/Pacific。这个当地时间将出现两次。如果用户勾选了“Re-运行 jobs when clocks fall back”,则该作业将执行两次,否则将执行一次。

为了进行上述计算,我需要知道我从前面提到的 post 中获得的夏令时转换日期。我还需要知道时钟将改变哪个方向以及改变多少(例如:1h)。

--更新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})";
        }
    }
}

听起来您 真正 寻找的是 DateTimeZone.MapLocal(LocalDateTime). That returns a ZoneLocalMapping,它告诉您本地 date/time 是如何映射到指定时区的。重要的属性是:

  • Count
    • 0 如果 date/time 被跳过
    • 1 如果它被明确映射
    • 2 如果映射不明确
  • EarlyIntervalLateInterval 是 before/after 指定 LocalDateTime 周围的任何过渡的 ZoneInterval 值(或相同的 ZoneInterval如果值不在转换中)

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

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

您还可以使用 DateTimeZone.ResolveLocal(LocalDateTime, ZoneLocalMappingResolver),根据用户的选择构建您自己的解析器(用于处理 skipped/ambiguous 次)。