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

问题描述

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

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

考虑以下场景:

  1. UtcDate - LocalDate,其中 LocalDate 比 DST 切换早 1 毫秒。

  2. LocalDateA - LocalDateB,其中 LocalDateB 比 DST 切换早 1 毫秒。

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));
}

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

  1. 如果日期在保存之前全部转换为 UTC,那么这种方法将无济于事。时区信息(以及 DST 的任何处理)丢失。我已经习惯于始终以 UTC 保存日期,DST 问题是否影响不足以做出错误的决定?

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

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

标签: c#datetimezonedst

解决方案


正如你所指出的,这个问题之前已经讨论过了。这里这里有两个很好的帖子可供审查。

此外,有关文档DateTime.Subtract有这样的说法:

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

笔记

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

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

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

如果你已经有了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扩展方法(我也在其他答案中使用过)。

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));
}

推荐阅读