I have two DateTime
objects which contain two UTC date/times and a users TimezoneId (tzdb) as a string
. I'm trying to write a method that takes these three parameters and returns the total seconds (or Duration
) between the two datetimes relative to the timezone.
public static double GetDurationForTimezone(DateTime startUtc, DateTime endUtc, string timezoneId)
{
var timezone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(timezoneId);
// convert UTC to timezone
var startInstantUtc = Instant.FromDateTimeUtc(startUtc);
var startZonedDateTime = startInstantUtc.InZone(timezone);
var endInstantUtc = Instant.FromDateTimeUtc(endUtc);
var endZonedDateTime = endInstantUtc.InZone(timezone);
return endZonedDateTime.ToInstant().Minus(startZonedDateTime.ToInstant()).ToTimeSpan().TotalSeconds;
}
I want to do it w.r.t. the timezone, to ensure it takes into account any possible Daylight Saving changes that may occur throughout this period.
Example test:
// DST starts (25h day -- DST starts: 10/4 @ 2am local time)
var result = GetDurationForTimezone(
new DateTime(2015, 10, 3, 15, 0, 0, DateTimeKind.Utc),
new DateTime(2015, 10, 4, 15, 0, 0, DateTimeKind.Utc),
"Australia/Sydney");
Assert.Equal(TimeSpan.FromHours(25).TotalSeconds, result);
But when running this test, it seems like the calls to .ToInstant()
are not adhering to the ZonedDateTime
versions, but rather the original UTC DateTime
objects. Thus I'm seeing the result be 24 hours.
When determining the duration between UTC-based timestamps, the time zone is irrelevant.
UTC is Coordinated Universal Time. It is the same for everyone on the planet. It does not have daylight saving time, and it's offset is always zero (UTC+00:00).
Since you have already asserted that the input values are in UTC, you do not necessarily need to use Noda Time for this operation. Just subtract the two values.
If you do use Noda Time, a UTC value is best represented by an
Instant
, which makes it very easy to obtain aDuration
.You could also represent them using
ZonedDateTime
values that happen to be "in UTC", however you'd quickly find that the API requires you convert them back toInstant
values to obtain aDuration
anyway.You might think that just using
LocalDateTime
would be an option, but that structure represents a wall time, without any time zone information. You can't obtain aDuration
between two of them. You could obtain aPeriod
by usingPeriod.Between
, but that would represent the calendar/clock-value difference between the two representations - which is not the same as the actual amount of time that has elapsed.As a thought exercise that will help understand the difference, consider these two values:
If I tell you that the values are in UTC, then there is one hour difference. However, if I tell you these are wall-clock values and they are in the US Eastern Time zone, then they might be one hour apart, or they might be two hours apart. It depends on whether or not the 01:30 is the one before the DST transition, or the one after - as there are two on this day.
Now if instead I gave you these values:
Again, if you interpret them as UTC they are exactly two hours apart. But if you interpret them in the same US Eastern time zone, then they are exactly three hours apart, because the range is inclusive of the DST transition. If you just subtract the local wall-time values then you'd get two hours, which would be incorrect.
Switching to utilize the
LocalDateTime
property of theZonedDateTime
allows for comparing the date/times relative to the timezone. This works for both prime test cases (23h and 25h days):Studying this page: ZonedDateTime.Comparer Members
it seems like you have to use property
Local
and notInstant
to reflect the local daylight savings.