首页 > 解决方案 > C# - 处理 DST 过渡日的主要时间范围 - 提供的 DateTime 表示无效时间

问题描述

几个前提:

  1. 我所说的“流行时间”是指它是如何在本地处理的(我的行业使用这个术语)。例如,东部流行时间的 UTC 偏移量为 -05:00,但 DST 期间为 -04:00
  2. 我发现通过将最终值视为排他性而不是骇人听闻的包容性方法(您必须从超出范围末尾的第一个值中减去一个epsilon )来处理范围数据要干净得多。

例如,根据区间表示法,从 0(包括)到 1(不包括)的值范围[0, 1)epsilon取决于所使用的数据类型)。[0, 0.99999999999...]

考虑到这两个想法,当结束时间戳无效(即没有凌晨 2 点,立即变为凌晨 3 点)时,如何表示春季 DST 过渡日的最后一小时时间范围?

[2019-03-10 01:00, 2019-03-10 02:00)在您选择的支持 DST 的时区。

将结束时间设置为03:00非常具有误导性,因为它看起来像是一个 2 小时宽的时间范围。

当我通过这个 C# 示例代码运行它时,它爆炸了:

DateTime hourEnd_tz = new DateTime(2019, 3, 10, 0, 0, 0, DateTimeKind.Unspecified);//midnight on the spring DST transition day
hourEnd_tz = hourEnd_tz.AddHours(2);//other code variably computes this offset from business logic
TimeZoneInfo EPT = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");//includes DST rules
DateTime hourEnd_utc = TimeZoneInfo.ConvertTime(//interpret the value from the user's time zone
    hourEnd_tz,
    EPT,
    TimeZoneInfo.Utc);

System.ArgumentException : '提供的 DateTime 表示无效时间。例如,当时钟向前调整时,被跳过的周期内的任何时间都是无效的。参数名称:日期时间'

我该如何处理这种情况(在其他地方我已经在处理秋天的模糊时间),而不必广泛重构我的时间范围类库?

标签: c#datetimedst

解决方案


前提 1 是合理的,尽管“普遍”这个词经常被删除,它只是被称为“东部时间”——两者都可以。

前提 2 是最佳实践。半开范围提供了许多好处,例如不必处理涉及 epsilon 的日期数学,或者不必确定 epsilon 应该具有的精度。

但是,您试图描述的范围不能仅通过日期和时间来完成。它还需要涉及与 UTC 的偏移量。对于美国东部时间(使用 ISO 8601 格式),它看起来像这样:

[2019-03-10T01:00:00-05:00, 2019-03-10T03:00:00-04:00)  (spring-forward)
[2019-11-03T02:00:00-04:00, 2019-11-03T02:00:00-05:00)  (fall-back)

你说:

将结束时间设置为 03:00 非常具有误导性,因为它看起来像是一个 2 小时宽的时间范围。

啊,但是将春季结束时间设置为 02:00 也会产生误导,因为当天没有观察到当地时间。只有将实际的本地日期和时间与当时的偏移量结合起来才能准确。

您可以使用DateTimeOffset.NET 中的结构对这些(或Noda TimeOffsetDateTime中的结构)进行建模。

我该如何处理这种情况......而不必广泛重构我的时间范围类库?

首先,您需要一个扩展方法,可以让您从特定时区转换为 a DateTimeDateTimeOffset你需要这个有两个原因:

  • 构造new DateTimeOffset(DateTime)函数假定 a DateTimewith KindofDateTimeKind.Unspecified应被视为本地时间。没有机会指定时区。

  • new DateTimeOffset(dt, TimeZoneInfo.GetUtcOffset(dt))方法还不够好,因为GetUtcOffset假定您在模棱两可或无效的情况下需要标准时间偏移。通常情况并非如此,因此您必须自己编写以下代码:

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

现在您已经定义了它(并将其放在项目中的某个静态类中),您可以在应用程序中需要的地方调用它。

例如:

TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime dt = new DateTime(2019, 3, 10, 2, 0, 0, DateTimeKind.Unspecified);
DateTimeOffset dto = dt.ToDateTimeOffset(tz);  // 2019-03-10T03:00:00-04:00

或者

TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime dt = new DateTime(2019, 11, 3, 1, 0, 0, DateTimeKind.Unspecified);
DateTimeOffset dto = dt.ToDateTimeOffset(tz);  // 2019-11-03T01:00:00-04:00

或者

TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime dt = new DateTime(2019, 3, 10, 0, 0, 0, DateTimeKind.Unspecified);

DateTimeOffset midnight = dt.ToDateTimeOffset(tz);                     // 2019-03-10T00:00:00-05:00
DateTimeOffset oneOClock = midnight.AddHours(1);                       // 2019-03-10T01:00:00-05:00
DateTimeOffset twoOClock = oneOClock.AddHours(1);                      // 2019-03-10T02:00:00-05:00
DateTimeOffset threeOClock = TimeZoneInfo.ConvertTime(twoOClock, tz);  // 2019-03-10T03:00:00-04:00

TimeSpan diff = threeOClock - oneOClock;  // 1 hour

请注意,DateTimeOffset正确减去两个值会考虑它们的偏移量(而减去两个DateTime值会完全忽略它们的Kind)。


推荐阅读