JamesFaix
JamesFaix

Reputation: 8655

DateTime.ToLocalTime not working properly with daylight savings

I have a DateTime instance with Kind = DateTimeKind.Utc and a timespan.

var dt = DateTime.UtcNow;
var ts = TimeSpan.FromDays(1);

When I localize dt and then add ts I get a different result than when I add ts and then localize, due to daylight savings.

var localizedFirst = dt.ToLocalTime() + ts; //Does account for daylight savings
var addedFirst = (dt + ts).ToLocalTime(); //Does not account for daylight savings

This seems very strange. Shouldn't adding an offset from localization and adding an offset from a timespan be commutative and associative?

I found a similar question: Why doesn't DateTime.ToLocalTime() take into account daylight savings? That question is dealing more with converting DateTime to and from String. I am working only with DateTime and TimeSpan arithmetic.

The best answer for that question suggested using DateTimeKind.Unspecified so that the runtime will assume the unspecified date is UTC and then it will convert it properly when localizing. I was very surprised that this actually worked. If I create a new DateTime like this:

var dt2 = new DateTime(dt.Ticks, DateTimeKind.Unspecified);

Then both orders of operations return the correct result with daylight savings.

(dt2 + ts).ToLocalTime() 
dt2.ToLocalTime() + ts

This all seems absurd to me. Why do I need to convert a Utc date to Unspecified just to convert it to Local properly? This seems like it should be considered a bug.

Other details:

Upvotes: 2

Views: 1715

Answers (2)

wardies
wardies

Reputation: 1259

This statement effectively asks for the day to be advanced but all other properties (hour, minute of day) to be left intact:

var localizedFirst = dt.ToLocalTime() + ts;

Whilst this statement asks what the local time will be after exactly 24 hours (elapsed time) have passed:

var addedFirst = (dt + ts).ToLocalTime();

It's a good argument for keeping everything in UTC until the last minute, then converting to Local Time for output.

Edit: Or conversely, if you don't want the local hours and minutes to change when you add or deduct days, convert to local time before adding the TimeSpan. However, as rightly pointed out by Matt Johnson, in this way you might end up with a local time that is either invalid (the clocks went forward over that time) or ambiguous (the clocks went back, so that time occurred twice). See his comment below for how to determine this.

Upvotes: 4

Matt Johnson-Pint
Matt Johnson-Pint

Reputation: 241525

A few points:

  • A TimeSpan represents an elapsed duration of time. Its "days" are standard days that are exactly 24 hours long.

  • On the day in question, in the local time zone, there were 25 hours because of the DST fall-back transition.

  • Addition on a DateTime object (either by + operator or Add... functions) is always done without regard for time zone. In other words, whatever the original .Kind property is, the output will have the same .Kind property, but the kind is not taken into consideration at all during addition/subtraction.

  • Thus, adding after converting to local time does not account for the 25 hour day. It is also problematic because it's possible to land on a local time value that does not exist, or exists twice.

So, when you say "does (or does not) account for daylight saving" in the code comments, technically you have it reversed. Since UTC has no transitions, the localizedFirst variable is the result of incorrectly assuming the local day is 24 hours long, while the addedFirst variable is the result of correctly applying the DST rules from the local time zone at the point that is 24 hours elapsed after the original point on the timeline.

Also, setting DateTimeKind.Unspecified will not change the effect for this case, because the DateTime.ToLocalTime() method will treat DateTimeKind.Unspecified as if it were DateTimeKind.Utc. See the table in the remarks of the documentation here. Indeed, I tried to replicate your results and could not get the value of dt2 to be any different just by changing the kind. If you can, please elaborate specifically on that point.

It's worth pointing out that eliminating this sort of confusion is exactly why the Noda Time library exists. In Noda Time, these are represented by two very different operations:

  • LocalDateTime + Period = LocalDateTime
  • Instant + Duration = Instant

Upvotes: 5

Related Questions