Time-travel with .NET or DateTime, DateTimeOffset and the lost DST hour [Greg]

Every year again comes the DST change...

And every year again do we need to work with customers on helping them understand some legacy design decisions, and how to work around these ghosts of the past.

Recently, around the end of October and the start of November the daylight saving time (DST) period finished in most areas of the northern hemisphere. As if it was an attempt to make up for the oncoming cold weather, we set our clocks back and got an extra hour of sleep. (In the southern hemisphere, the DST begins at the end of the year, and they turn their clocks forwards.) And one of the basic .NET types – DateTime – does not play well with the annual end-of-DST transition. This issue is well-known and so is the workaround, but nevertheless it gets reported to us regularly by customers who rediscover the issue, especially around the DST transition season. There are a number of resources that treat this topic in great detail. Some of these resources are listed below. Here, I provide some of the background for the reasons behind the issues and a brief refresher on how to solve the problems.

First the gist: The DateTimeOffset type helps avoiding most of the problems associated with DateTime. If you have trouble with DateTime in the context of a DST transition, consider using DateTimeOffset instead.

Issue: Incorrect time zone display.

One issue with the DateTime structure is that during transition from summer time to winter time (i.e. from DST to no-DST), the winter-time time-zone-offset it displayed an hour too early (when shown using a default built-in functionality such as DateTime``.``Now``.``ToString(``"o"``) ). However, the actual clock time is always displayed correctly. This symptom is caused by the interplay of the fact that DateTime stores the local time only, and the nature of DST transitions.

Similar to the DateTime type, the Windows DST database, on which Windows and .NET applications base their time calculations, specifies the transition times in local time (not UTC). During summer-to-winter time transition in the northern hemisphere the clocks are typically set one hour back. Assume, w.l.o.g., that the transition occurs at 3:00. Then, the times between 2:00 and 2:59 will occur twice: once during summer time, and once again during winter time after the clock has been set back. When the system is queried whether a time, say, 2:28, is in summer (say UTC -7) or in winter time (UTC -8), there is no way for the system to know which one is correct. In such cases, .NET assumes winter time, as that is typically the default time for the time zone (i.e. no DST). Note that if we changed this behaviour, the problem would still occur, only the other way around. For instance, the old DST state would show up one hour too long.

In this particular case, the actual time is always shown correctly, it is just the DST state that is incorrect.

Solution: Track the DST mode changes.

At one second past the hour (or any other pre-specified time), store the local system time. Then compare the current time against the timestamp stored when the one-second-part-the-hour event occurred previously. If they match, we know that we just transitioned to winter time: the offset has changed (e.g. using the previous example, to UTC-8). Otherwise, we are still in summer time: it is still UTC-7.

This workaround is complex and applies only to applications that run for at least one hour. This is not the best approach for a general fix.

Better solution: Use a type that stores time as UTC instead of local time.

UTC (coordinated universal time) describes the time at the Greenwich meridian and does not undergo any DST transitions. In a way (simplified but appropriate for this context), it can be understood as the “universal cosmological time”. The DateTimeOffset type stores the local time as a pair of values: one value describes the UTC time, and the other value describes the offset between UTC and the local time. For instance, 02:28 in the UTC-7 time zone is stored as {UtcTime - Offset} = {10:28 - 420minutes}. 02:28 in the UTC-8 time zone is stored as {11:28 - 480minutes}. In contrast to DateTime, these two values, although describing the same local time, describe two different points in time, and can be easily differentiated from each other. As a result, issues related to DST transitions can be handled correctly when using DateTimeOffset.

Putting it all together: Recommendations.

The reading list below contains a wealth of background information on the DateTime and DateTimeOffset types and how to use them. Here is a short and practical summary of how these types should be used.

Use DateTime to: Use DateTimeOffset to:
  • Represent dates only (no time information).
  • Represent times only (not on a specific date). (Alternatively, consider using the TimeSpan type to represent the time of day without a specific date context).
  • Work with abstract times and dates, such as historical dates.
  • Work with legacy APIs that do not support DateTimeOffset (e.g. some database and file system APIs).
  • Uniquely and unambiguously identify a single point in time.
  • Log events occurring at specific times.
  • Perform general date and time arithmetic.
  • Work with time zones.
  • Work with daylight saving times.

More guidance on choosing the best date and time type for your circumstances is provided through the links below.

Further reading: