问题
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:
UtcDate - LocalDate where LocalDate is 1 millisecond earlier than a DST switchover.
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:
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?
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?
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 theKind
property of the twoDateTime
values when performing the subtraction. Before subtractingDateTime
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