-2

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.

tris
  • 1,780
  • 3
  • 18
  • 28
  • 2
    If both of your inputs are always in UTC, and you want the elapsed time between them, you should not involve time zones. UTC is the same for the whole world, and does not have DST. – Matt Johnson-Pint Sep 28 '15 at 13:34
  • 1
    The test is flawed as well. No matter which time zone you are in locally, there are exactly 24 hours between those two UTC based values. – Matt Johnson-Pint Sep 28 '15 at 13:37

3 Answers3

3

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.

TimeSpan duration = endUtc - startUtc;

If you do use Noda Time, a UTC value is best represented by an Instant, which makes it very easy to obtain a Duration.

Instant start = Instant.FromDateTimeUtc(startUtc);
Instant end = Instant.FromDateTimeUtc(endUtc);
Duration duration = end - start;

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 to Instant values to obtain a Duration anyway.

ZonedDateTime start = LocalDateTime.FromDateTime(startUtc).InUtc();
ZonedDateTime end = LocalDateTime.FromDateTime(endUtc).InUtc();
Duration duration = end.ToInstant() - start.ToInstant();

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 a Duration between two of them. You could obtain a Period by using Period.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:

2015-11-01 00:30
2015-11-01 01:30

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:

2015-11-01 00:30
2015-11-01 02:30

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.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • 1
    Thanks for the detailed response, Matt. I'm actually after the 'calendar/clock-value' difference for that timezone. So the `Period` version is what I went with. If I understand your reply correctly, that is what you would use for that scenario too? – tris Sep 29 '15 at 01:09
  • Yes, that's what I would use. Your answer has the correct implementation for that particular problem. Though I'm skeptical of the use case that would want that. I hope you're not paying employee wages based on the result. :o – Matt Johnson-Pint Sep 29 '15 at 03:26
1

Switching to utilize the LocalDateTime property of the ZonedDateTime allows for comparing the date/times relative to the timezone. This works for both prime test cases (23h and 25h days):

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 startLocalDateTime = startZonedDateTime.LocalDateTime;

    var endInstantUtc = Instant.FromDateTimeUtc(endUtc);
    var endZonedDateTime = endInstantUtc.InZone(timezone);
    var endLocalDateTime = endZonedDateTime.LocalDateTime;

    return Period.Between(startLocalDateTime, endLocalDateTime, PeriodUnits.Seconds).Seconds;
}
tris
  • 1,780
  • 3
  • 18
  • 28
  • 1
    This gets the difference between the two wall times, not the elapsed duration between them. It *hides* the DST gap or overlap, rather than accounting for it. – Matt Johnson-Pint Sep 28 '15 at 13:41
0

Studying this page: ZonedDateTime.Comparer Members

it seems like you have to use property Local and not Instant to reflect the local daylight savings.

Gustav
  • 53,498
  • 7
  • 29
  • 55
  • Thanks Gustav, looks like that pointed me in the correct direction. Will add an answer. – tris Sep 28 '15 at 12:22