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