Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
762 views
in Technique[技术] by (71.8m points)

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

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...