I'm playing with some C# code to try to gain an understanding of how subtracting DateTime objects in C# works with respect to Daylight Saving Time.
Per Google and other sources, the Daylight Saving Time "spring ahead" event in the Eastern Standard Time zone in 2017 was at 2:00am on March 12. So, the first few hours of the day on that date were:
12:00am - 1:00am
1:00am - 2:00am
(There was no 2:00am - 3:00am hour due to the "spring ahead")
3:00am - 4:00am
So, if I were to calculate the time differential between 1:00am and 4:00am in that time zone on that date, I'd expect the result to be 2 hours.
However, the code I put together to try to simulate this problem is returning a 3 hour TimeSpan.
Code:
TimeZoneInfo easternStandardTime = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime oneAm = TimeZoneInfo.ConvertTime(new DateTime(2017, 03, 12, 01, 00, 00), easternStandardTime);
DateTime fourAm = TimeZoneInfo.ConvertTime(new DateTime(2017, 03, 12, 04, 00, 00), easternStandardTime);
TimeSpan difference = (fourAm - oneAm);
Console.WriteLine(oneAm);
Console.WriteLine(fourAm);
Console.WriteLine(TimeZoneInfo.Local.IsDaylightSavingTime(oneAm));
Console.WriteLine(TimeZoneInfo.Local.IsDaylightSavingTime(fourAm));
Console.WriteLine(difference);
On my PC, this generates:
2017-03-12 01:00:00.000 -5
2017-03-12 04:00:00.000 -4
False
True
03:00:00
All of that output is as expected -- except that final value of 3 hours, which as I noted above, I would expect to be 2 hours instead.
Obviously, my code isn't correctly simulating the situation that I have in mind. What is the flaw?
Observe:
// These are just plain unspecified DateTimes
DateTime dtOneAm = new DateTime(2017, 03, 12, 01, 00, 00);
DateTime dtFourAm = new DateTime(2017, 03, 12, 04, 00, 00);
// The difference is not going to do anything other than 4-1=3
TimeSpan difference1 = dtFourAm - dtOneAm;
// ... but we have a time zone to consider!
TimeZoneInfo eastern = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
// Use that time zone to get DateTimeOffset values.
// The GetUtcOffset method has what we need.
DateTimeOffset dtoOneAmEastern = new DateTimeOffset(dtOneAm, eastern.GetUtcOffset(dtOneAm));
DateTimeOffset dtoFourAmEastern = new DateTimeOffset(dtFourAm, eastern.GetUtcOffset(dtFourAm));
// Subtracting these will take the offset into account!
// It essentially does this: [4-(-4)]-[1-(-5)] = 8-6 = 2
TimeSpan difference2 = dtoFourAmEastern - dtoOneAmEastern;
// Let's see the results
Console.WriteLine("dtOneAm: {0:o} (Kind: {1})", dtOneAm, dtOneAm.Kind);
Console.WriteLine("dtFourAm: {0:o} (Kind: {1})", dtFourAm, dtOneAm.Kind);
Console.WriteLine("difference1: {0}", difference1);
Console.WriteLine("dtoOneAmEastern: {0:o})", dtoOneAmEastern);
Console.WriteLine("dtoFourAmEastern: {0:o})", dtoFourAmEastern);
Console.WriteLine("difference2: {0}", difference2);
Results:
dtOneAm: 2017-03-12T01:00:00.0000000 (Kind: Unspecified)
dtFourAm: 2017-03-12T04:00:00.0000000 (Kind: Unspecified)
difference1: 03:00:00
dtoOneAmEastern: 2017-03-12T01:00:00.0000000-05:00)
dtoFourAmEastern: 2017-03-12T04:00:00.0000000-04:00)
difference2: 02:00:00
Note that DateTime
carries a DateTimeKind
in its Kind
property, which is Unspecified
by default. It doesn't belong to any particular time zone. DateTimeOffset
doesn't have a kind, it has an Offset
, which tells you how far that local time is offset from UTC. Neither of these give you the time zone. That is what TimeZoneInfo
object is doing. See "time zone != offset" in the timezone tag wiki.
The part I think you are perhaps frustrated with, is that for several historical reasons, the DateTime
object does not ever understand time zones when doing math, even when you might have DateTimeKind.Local
. It could have been implemented to observe the transitions of the local time zone, but it was not done that way.
You also might be interested in Noda Time, which gives a very different API for date and time in .NET, in a much more sensible and purposeful way.
using NodaTime;
...
// Start with just the local values.
// They are local to *somewhere*, who knows where? We didn't say.
LocalDateTime ldtOneAm = new LocalDateTime(2017, 3, 12, 1, 0, 0);
LocalDateTime ldtFourAm = new LocalDateTime(2017, 3, 12, 4, 0, 0);
// The following won't compile, because LocalDateTime does not reference
// a linear time scale!
// Duration difference = ldtFourAm - ldtOneAm;
// We can get the 3 hour period, but what does that really tell us?
Period period = Period.Between(ldtOneAm, ldtFourAm, PeriodUnits.Hours);
// But now lets introduce a time zone
DateTimeZone eastern = DateTimeZoneProviders.Tzdb["America/New_York"];
// And apply the zone to our local values.
// We'll choose to be lenient about DST gaps & overlaps.
ZonedDateTime zdtOneAmEastern = ldtOneAm.InZoneLeniently(eastern);
ZonedDateTime zdtFourAmEastern = ldtFourAm.InZoneLeniently(eastern);
// Now we can get the difference as an exact elapsed amount of time
Duration difference = zdtFourAmEastern - zdtOneAmEastern;
// Dump the output
Console.WriteLine("ldtOneAm: {0}", ldtOneAm);
Console.WriteLine("ldtFourAm: {0}", ldtFourAm);
Console.WriteLine("period: {0}", period);
Console.WriteLine("zdtOneAmEastern: {0}", zdtOneAmEastern);
Console.WriteLine("zdtFourAmEastern: {0}", zdtFourAmEastern);
Console.WriteLine("difference: {0}", difference);
ldtOneAm: 3/12/2017 1:00:00 AM
ldtFourAm: 3/12/2017 4:00:00 AM
period: PT3H
zdtOneAmEastern: 2017-03-12T01:00:00 America/New_York (-05)
zdtFourAmEastern: 2017-03-12T04:00:00 America/New_York (-04)
difference: 0:02:00:00
We can see a period of three hours, but it doesn't really mean the same as the elapsed time. It just means the two local values are three hours apart in their position on a clock. NodaTime understands the difference between these concepts, while .Net's built-in types do not.
Some follow-up reading for you:
- What's wrong with DateTime anyway?
- More Fun with DateTime
- The case against DateTime.Now
- Five Common Daylight Saving Time Antipatterns of .NET Developers
Oh, and one other thing. Your code has this...
DateTime oneAm = TimeZoneInfo.ConvertTime(new DateTime(2017, 03, 12, 01, 00, 00), easternStandardTime);
Since the DateTime
you create has unspecified kind, you are asking to convert from your computer's local time zone to Eastern time. If you happen to be not in Eastern time, your oneAm
variable might not be 1 AM at all!
So this is addressed in the MSDN documentation.
Basicaly, when subtracting one date from another you should be using DateTimeOffset.Subtract()
and not arithmetic subtraction as you have here.
TimeSpan difference = fourAm.Subtract(oneAm);
Yields the expected 2 hour time difference.
Ok, so I made some minor changes to your code. Not sure if this is what you are trying to achieve or not but this will give you what you want...
static void Main() {
TimeZoneInfo easternStandardTime = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
TimeZone timeZone = TimeZone.CurrentTimeZone;
DateTime oneAm = TimeZoneInfo.ConvertTime(new DateTime(2017, 03, 12, 01, 00, 00), easternStandardTime);
DateTime fourAm = TimeZoneInfo.ConvertTime(new DateTime(2017, 03, 12, 04, 00, 00), easternStandardTime);
DaylightTime time = timeZone.GetDaylightChanges(fourAm.Year);
TimeSpan difference = ((fourAm - time.Delta) - oneAm);
Console.WriteLine(oneAm);
Console.WriteLine(fourAm);
Console.WriteLine(TimeZoneInfo.Local.IsDaylightSavingTime(oneAm));
Console.WriteLine(TimeZoneInfo.Local.IsDaylightSavingTime(fourAm));
Console.WriteLine(difference);
Console.ReadLine();
}