Here, we look look at the effect of the daylight saving time (DST) on Local DateTime values, the best practice of using Utc DateTime values, what makes our best practice harder to follow, and how one should specify AssumeLocal (or AssumeUniversal) and AdjustToUniversal.
As a .NET developer, I am used to the .NET class library being really well architected.
This is a double edged sword because, when DateTime, one of the most used types in any application, starts misbehaving, it may catch you by surprise.
1985, Hill Valley CA
If you want to follow along the code samples in this post, make sure to set your computer’s time zone to Pacific Time (US & Canada).
Photo by Søren Lundtoft, marked as Free Use
Anybody, who had to suffer through using dates and times in Java, likely appreciates the ease of having a single DateTime
class in .NET representing both Utc and Local times, as well as the mysterious “Unspecified” times.
Unfortunately, the DateTime
type shows some very unexpected and error prone behavior when working with anything except for Utc times.
Always use Utc DateTime
values!
Local DateTime
values should be limited to the UI layer only.
The Temporal Displacement Occurred Exactly 1:00 Am and Zero Seconds
I will start this post by looking at the effect of the daylight saving time (DST) on Local DateTime
values.
In the code snippets below, I will show the full content of each DateTime
value as a comment. You can use this extension method to achieve the same formatting.
public static class DateTimeUtils
{
public static void Print(this DateTime t) =>
Console.WriteLine(t.ToString("yyyy-MM-dd HH:mm") + " " +
t.Kind + " " +
(t.IsDaylightSavingTime() ? "DST" : "ST"));
}
The following timestamps are 30 minutes before and after the DST change in Pacific Time. Because you turn the clock back one hour when leaving DST, both their local times are 1:30AM.
var utc1 = new DateTime(1985, 10, 27, 8, 30, 0, DateTimeKind.Utc);
var utc2 = new DateTime(1985, 10, 27, 9, 30, 0, DateTimeKind.Utc);
var local1 = utc1.ToLocalTime();
var local2 = utc2.ToLocalTime();
So utc1
and utc2
are 1 hour apart and the same is true for local1
and local2
because they represent the same instants. No information is lost in the conversion to local time: local1
and local2
can be correctly converted back to UTC.
local1.ToUniversalTime();
local2.ToUniversalTime();
Things get weird when we start to use operators. Both equality and arithmetic operators work well for Utc times but are broken for Local times or a mix of the two:
Console.WriteLine(utc1 == utc2);
Console.WriteLine(utc2 - utc1);
Console.WriteLine(local1 == local2);
Console.WriteLine(local2 - local1);
Console.WriteLine(local1 - utc1);
This behavior is partially documented but it is nonetheless extremely error-prone:
“The Equality operator determines whether two DateTime values are equal by comparing their number of ticks. Before comparing DateTime objects, make sure that the objects represent times in the same time zone. You can do this by comparing the values of their Kind property.“
Great Scott!
Most of these inconsistencies are only triggered for two hours in the whole year, during the DST changes. They are very likely to be missed by unit tests or QA.
Image by Emanhattan, used under Creative Commons license
Best Practices
DateTime
issues are avoided with one, easy to follow, best practice: only use Utc DateTime
values!
The use of Local DateTime
values should be limited to the UI layer only. If you are using WPF or some other UI framework which supports data binding, you could consider using Utc all the way up to the UI and bake the conversion to/from Local into the data biding itself.
In a previous post, I even played around with the idea of not using DateTime
at all and replace it with a compatible type that only allows Utc times:
public struct UtcDateTime
{
private readonly DateTime Time;
public UtcDateTime(DateTime time)
{
switch (time.Kind)
{
case DateTimeKind.Utc:
Time = time;
break;
case DateTimeKind.Local:
Time = time.ToUniversalTime();
break;
default:
throw new NotSupportedException
("UtcDateTime cannot be initialized with an Unspecified DateTime.");
}
}
public static implicit operator UtcDateTime(DateTime t) => new UtcDateTime(t);
public static implicit operator DateTime(UtcDateTime t) => t.Time;
}
DateTime Parsing Gives Us Back the Wrong 1985
(In which Biff is corrupt, and powerful...)
What makes our best practice harder to follow is that the Parse and ParseExact methods have a tendency to return Local, or even Unspecified, DateTime
values, even when the input string represents a Utc time.
DateTime.Parse("1985-10-27T01:30:00", CultureInfo.InvariantCulture);
DateTime.Parse("1985-10-27T08:30:00", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal);
DateTime.Parse("1985-10-27T08:30:00Z", CultureInfo.InvariantCulture);
DateTime.ParseExact("1985-10-27T08:30:00Z", "yyyy-MM-ddTHH:mm:ss\\Z",
CultureInfo.InvariantCulture);
DateTime.ParseExact("1985-10-27T08:30:00Z", "yyyy-MM-ddTHH:mm:ssZ",
CultureInfo.InvariantCulture);
I particularly dislike using Unspecified DateTime
values because behave inconsistently when converted to Local and Utc.
var t = new DateTime(1985, 10, 27, 8, 30, 0);
t.ToLocalTime();
t.ToUniversalTime();
The best practice to avoid all the complexity around parsing DateTime
values is to always specify AssumeLocal (or AssumeUniversal
if it is more appropriate for your use case) and AdjustToUniversal. This makes parsing behave much more consistently and always return Utc values that are ready to be stored or used across the application.
DateTime.Parse("1985-10-27T01:30:00", CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal);
DateTime.Parse("1985-10-27T01:30:00", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeLocal |
DateTimeStyles.AdjustToUniversal);
DateTime.Parse("1985-10-27T08:30:00", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal |
DateTimeStyles.AdjustToUniversal);
DateTime.Parse("1985-10-27T08:30:00Z", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeLocal |
DateTimeStyles.AdjustToUniversal);
DateTime.ParseExact("1985-10-27T08:30:00Z", "yyyy-MM-ddTHH:mm:ss\\Z",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeLocal |
DateTimeStyles.AdjustToUniversal);
DateTime.ParseExact("1985-10-27T08:30:00Z", "yyyy-MM-ddTHH:mm:ss\\Z",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal |
DateTimeStyles.AdjustToUniversal);
Whoa, This is Heavy!
While all of this is pretty basic stuff, plenty of experienced engineers, me included, are tripped by this from time to time. The effects can be pretty bad, including crashes and data loss or corruption.
Hopefully, you found this information useful, and scary enough to remember to follow the best practices :-).
Thanks for reading!