在计算 DateTimes 之间的持续时间时安全处理夏令时(或任何其他理论上的非常量偏移)

Posted

技术标签:

【中文标题】在计算 DateTimes 之间的持续时间时安全处理夏令时(或任何其他理论上的非常量偏移)【英文标题】:Safe handling of daylight savings (or any other theoretical non-constant offset) while calculating durations between DateTimes 【发布时间】:2020-05-07 20:56:35 【问题描述】:

我知道即使在过去的 24 小时内,这也不是第一次提出这个话题,但我很惊讶我还没有找到一个明确/最佳实践的解决方案来解决这个问题。这个问题似乎也与我认为将所有日期保存为 UTC 的不费吹灰之力的设计决定相矛盾。我将尝试在这里陈述问题:

给定两个 DateTime 对象,找出它们之间的持续时间,同时考虑夏令时。

考虑以下场景:

    UtcDate - LocalDate 其中 LocalDate 比 a 早 1 毫秒 夏令时切换。

    LocalDateA - LocalDateB,其中 LocalDateB 为 1 比 DST 切换早几毫秒。

UtcDate - LocalDate.ToUtc() 提供不考虑 DST 开关的持续时间。 LocalDateA.ToUtc() - LocalDateB.ToUtc() 是正确的,但 LocalDateA - LocalDateB 也忽略 DST。

现在,显然个解决方案可以解决这个问题。我现在使用的解决方案是这种扩展方法:

public static TimeSpan Subtract(this DateTime minuend, TimeZoneInfo minuendTimeZone, 
    DateTime subtrahend, TimeZoneInfo subtrahendTimeZone)

    return TimeZoneInfo.ConvertTimeToUtc(DateTime.SpecifyKind(minuend, 
        DateTimeKind.Unspecified), minuendTimeZone)
        .Subtract(TimeZoneInfo.ConvertTimeToUtc(DateTime.SpecifyKind(subtrahend, 
            DateTimeKind.Unspecified), subtrahendTimeZone));

我猜它有效。不过我有一些问题:

    如果日期在保存之前全部转换为 UTC,那么这个 方法无济于事。时区信息(以及任何处理 DST) 丢失。我已经习惯于始终以 UTC 保存日期,是 DST 问题的影响力不足以使其变得糟糕 决定?

    不太可能有人知道这种方法,甚至 考虑到这个问题,在计算两者之间的差异时 日期。有没有更安全的解决方案?

    如果我们齐心协力,也许科技行业可以说服 国会废除夏令时。

【问题讨论】:

如果您使用NodaTime,则很难弄错,因为如果没有涉及明确意图的转换,就不可能混合 UTC 和本地。 Jon Skeet 的一些教育读物 - codeblog.jonskeet.uk/2019/03/27/… ......基本上解释了你不能真正计算时间之间的差异,特别是在未来 100% 正确的情况下,因为规则可能会改变。所以你永远不知道“我每天早上 9 点喝咖啡,我喝咖啡休息多长时间”是否意味着 23,24 或 25 小时或两者之间的任何时间(转换为 UTC 会使情况变得更糟):( 正如您所指出的,这已经讨论了 很多次 次。请查看链接的副本。也是in the remarks of these docs。基本上,改用DateTimeOffset 是的,DateTimeOffset 有一个固定的偏移量,但这意味着即使两个偏移量不同,减法也能正常工作(在减法之前将它们标准化为 UTC)。是的,我看到您的代码正在改变种类。这是一个问题。考虑subtrahend 是否为DateTimeKind.Utcminuend 是否为DateTimeKind.Local(并且本地时区不是UTC)。您不会获得实际经过时间的差异,而只会获得本地时钟时间与您的代码的差异。 (它甚至可能是负面的。) 我重新打开了您的问题,以便在下面添加更详细的答案。谢谢。 【参考方案1】:

正如你所指出的,这个问题之前已经讨论过了。 Here 和 here 是两个值得审查的好帖子。

另外,DateTime.Subtract 上的the documentation 有这样的说法:

Subtract(DateTime) 方法在执行减法时不考虑两个DateTime 值的Kind 属性的值。在减去DateTime 对象之前,请确保对象代表同一时区的时间。否则,结果将包括时区之间的差异。

注意

DateTimeOffset.Subtract(DateTimeOffset) 方法确实在执行减法时考虑了时区之间的差异。

除了“表示同一时区的时间”之外,请记住,即使对象处于同一时区,DateTime 值的减法仍然不会考虑 DST 或两个对象之间的其他转换。

关键是要确定经过的时间,您应该减去绝对时间点。这些最好用 .NET 中的 DateTimeOffset 表示。

如果您已经有 DateTimeOffset 值,则可以减去它们。但是,您仍然可以使用 DateTime 值,只要您先正确地将它们转换为 DateTimeOffset

或者,您可以将所有内容都转换为 UTC - 但无论如何您都必须通过 DateTimeOffset 或类似代码才能正确执行此操作。

在您的情况下,您可以将代码更改为以下内容:

public static TimeSpan Subtract(this DateTime minuend, TimeZoneInfo minuendTimeZone, 
    DateTime subtrahend, TimeZoneInfo subtrahendTimeZone)

    return minuend.ToDateTimeOffset(minuendTimeZone) -
        subtrahend.ToDateTimeOffset(subtrahendTimeZone);

您还需要 ToDateTimeOffset 扩展方法 (which I've also used on other answers)。

public static DateTimeOffset ToDateTimeOffset(this DateTime dt, TimeZoneInfo tz)

    if (dt.Kind != DateTimeKind.Unspecified)
    
        // Handle UTC or Local kinds (regular and hidden 4th kind)
        DateTimeOffset dto = new DateTimeOffset(dt.ToUniversalTime(), TimeSpan.Zero);
        return TimeZoneInfo.ConvertTime(dto, tz);
    

    if (tz.IsAmbiguousTime(dt))
    
        // Prefer the daylight offset, because it comes first sequentially (1:30 ET becomes 1:30 EDT)
        TimeSpan[] offsets = tz.GetAmbiguousTimeOffsets(dt);
        TimeSpan offset = offsets[0] > offsets[1] ? offsets[0] : offsets[1];
        return new DateTimeOffset(dt, offset);
    

    if (tz.IsInvalidTime(dt))
    
        // Advance by the gap, and return with the daylight offset  (2:30 ET becomes 3:30 EDT)
        TimeSpan[] offsets =  tz.GetUtcOffset(dt.AddDays(-1)), tz.GetUtcOffset(dt.AddDays(1)) ;
        TimeSpan gap = offsets[1] - offsets[0];
        return new DateTimeOffset(dt.Add(gap), offsets[1]);
    

    // Simple case
    return new DateTimeOffset(dt, tz.GetUtcOffset(dt));

【讨论】:

因为您要删除该类型,因此必须提前假定该时区的时间已经正确。例如,如果 minuend 已经是 UTC,但 minuendTimeZone 来自 TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time") 或其他时区,那么 ConvertTimeToUtc 执行的转换将不正确。 Kind 属性可以防止这种情况发生,至少对于 UTC 和本地时区而言。它在其他时区表现不佳,这就是 DateTimeOffset 效果更好的原因。

以上是关于在计算 DateTimes 之间的持续时间时安全处理夏令时(或任何其他理论上的非常量偏移)的主要内容,如果未能解决你的问题,请参考以下文章

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

当某些记录不完整时,计算登录和注销之间的持续时间

防火墙基础之企业之间互通

防火墙基础之分支与分支之间互通

防火墙基础之终端与服务器之间互通

在javascript中计算两个日期时间之间的持续时间