Reputation: 8655
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:
dt
: 11/5/2017 2:36:13pm UTC
ts
: TimeSpan.FromDays(699)
dt
: 11/5/2017 9:36:13am
(dt + ts).ToLocalTime()
: 10/5/2019 10:36:13am
dt.ToLocalTime() + ts
: 10/5/2019 9:36:13am
Upvotes: 2
Views: 1715
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
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