Safe handling of daylight savings (or any other theoretical non-constant offset) while calculating durations between DateTimes

眉间皱痕 提交于 2021-02-15 07:33:30

问题


I know this isn't the first time this topic has been brought up even in the past 24 hours, but I'm surprised that I have not come across one clear / best practices solution to this problem. The problem also seems to contradict what I thought was a no-brainer design decision to save all dates in UTC. I'll try to state the problem here:

Given two DateTime objects, find the duration between them while accounting for daylight savings.

Consider the following scenarios:

  1. UtcDate - LocalDate where LocalDate is 1 millisecond earlier than a DST switchover.

  2. LocalDateA - LocalDateB where LocalDateB is 1 millisecond earlier than a DST switchover.

UtcDate - LocalDate.ToUtc() provides a duration that did not consider the DST switch. LocalDateA.ToUtc() - LocalDateB.ToUtc() is correct, but LocalDateA - LocalDateB also disregards DST.

Now, there obviously are solutions to this problem. The solution that I'm using now is this extension method:

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

It works, I guess. I have some problems with it though:

  1. If dates are all converted to UTC before being saved, then this method won't help. The timezone information (and any handling of DST) is lost. I've been conditioned to always save dates in UTC, is the issue of DST just not impactful enough to make that a bad decision?

  2. It's unlikely that someone will be aware of this method, or even thinking about this problem, when calculating the difference between dates. Is there a safer solution?

  3. If we all work together, maybe the tech industry can convince congress to abolish daylight savings.


回答1:


As you pointed out, this has been discussed before. Here and here are two good posts to review.

Also, the documentation on DateTime.Subtract has this to say:

The Subtract(DateTime) method does not consider the value of the Kind property of the two DateTime values when performing the subtraction. Before subtracting DateTime objects, ensure that the objects represent times in the same time zone. Otherwise, the result will include the difference between time zones.

Note

The DateTimeOffset.Subtract(DateTimeOffset) method does consider the difference between time zones when performing the subtraction.

Beyond just "represent times in the same time zone", keep in mind that even if the objects are in the same time zone, the subtraction of DateTime values will still not consider DST or other transitions between the two objects.

The key point is that to determine the time elapsed, you should be subtracting absolute points in time. These are best represented by a DateTimeOffset in .NET.

If you already have DateTimeOffset values, you can just subtract them. However, you can still work with DateTime values as long as you first convert them to a DateTimeOffset properly.

Alternatively, you could convert everything to UTC - but you'd have to go through DateTimeOffset or similar code to do that properly anyway.

In your case, you can change your code to the following:

public static TimeSpan Subtract(this DateTime minuend, TimeZoneInfo minuendTimeZone, 
    DateTime subtrahend, TimeZoneInfo subtrahendTimeZone)
{
    return minuend.ToDateTimeOffset(minuendTimeZone) -
        subtrahend.ToDateTimeOffset(subtrahendTimeZone);
}

You will also need the ToDateTimeOffset extension method (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));
}


来源:https://stackoverflow.com/questions/59847174/safe-handling-of-daylight-savings-or-any-other-theoretical-non-constant-offset

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