C# - Handling ranges of prevailing times on DST transition days - The supplied DateTime represents an invalid time

六月ゝ 毕业季﹏ 提交于 2020-01-30 03:00:34

问题


A couple of premises:

  1. By "prevailing time" I mean how it is handled locally (my industry uses this terminology). For example, Eastern Prevailing Time has a UTC offset of -05:00 except during DST when it is -04:00
  2. I find it much cleaner to handle range data by treating the end value as exclusive, rather than the hackish inclusive approach (where you have to subtract an epsilon from the first value beyond the end of your range).

For example, the range of values from 0 (inclusive) to 1 (exclusive), as per interval notation, is [0, 1), which is much more readable than [0, 0.99999999999...] (and is less prone to rounding issues and thus off-by-one errors, because the epsilon value depends on the data type being used).

With these two ideas in mind, how can I represent the final hour time range on the spring DST transition day, when the ending timestamp is invalid (i.e. there is no 2am, it instantly becomes 3am)?

[2019-03-10 01:00, 2019-03-10 02:00) in your time zone of choice that supports DST.

Putting the end time as 03:00 is quite misleading, as it looks like a 2-hour wide time range.

When I run it through this C# sample code, it blows up:

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: 'The supplied DateTime represents an invalid time. For example, when the clock is adjusted forward, any time in the period that is skipped is invalid. Parameter name: dateTime'

How might I handle this case (elsewhere I am already handling the autumn ambiguous times), without having to extensively refactor my time range class library?


回答1:


Premise 1 is reasonable, though often the word "prevailing" is dropped and it's just called "Eastern Time" - either are fine.

Premise 2 is a best practice. Half-open ranges offer many benefits, such as not having to deal with date math involving an epsilon, or having to determine what precision the epsilon should have.

However, the range you're attempting to describe cannot be done with a date and time alone. It needs to also involve the offset from UTC. For US Eastern Time (using ISO 8601 format), it looks like this:

[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)

You said:

Putting the end time as 03:00 is quite misleading, as it looks like a 2-hour wide time range.

Ah, but putting the spring end time as 02:00 would also be misleading, as that local time is not observed on that day. Only by combining the actual local date and time with the offset at that time can one be accurate.

You can use the DateTimeOffset structure in .NET to model these (or the OffsetDateTime structure in Noda Time).

How might I handle this case ... without having to extensively refactor my time range class library?

First, you'll need an extension method that lets you convert from DateTime to a DateTimeOffset for a specific time zone. You'll need this for two reasons:

  • The new DateTimeOffset(DateTime) constructor assumes that a DateTime with Kind of DateTimeKind.Unspecified should be treated as local time. There's no opportunity to specify a time zone.

  • The new DateTimeOffset(dt, TimeZoneInfo.GetUtcOffset(dt)) approach isn't good enough, because GetUtcOffset presumes you want the standard time offset in the case of ambiguity or invalidity. That is usually not the case, and thus you have to code the following yourself:

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

Now that you have that defined (and put it in a static class somewhere in your project), you can call it where needed in your application.

For example:

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

or

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

or

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

Note that subtracting two DateTimeOffset values correctly considers their offsets (whereas subtracting two DateTime values completely ignores their Kind).



来源:https://stackoverflow.com/questions/57878720/c-sharp-handling-ranges-of-prevailing-times-on-dst-transition-days-the-suppl

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!