Reputation: 430
We encountered a problem in our app with a specific date in a specific TimeZone where in a round-trip from the server to client and then from the client to the server the value of the DateTime was not preserved. This was observed in the Brasilia timezone (“E. South America Standard Time”) and the DateTime value is “1984-11-04 00:00:00”.
I was able to reproduce this problem with the following code:
DateTime d = new DateTime(1984, 11, 4, 0, 0, 0, DateTimeKind.Local);
var dUtc = d.ToUniversalTime();
var dRtLocal = dUtc.ToLocalTime();
The final value of dUTC is “1984-11-04 03:00:00” (correct) and dRtLocal is “1984-11-04 01:00:00” (not so correct).
I’ve found that although Daylight Saving in Brazil only started in 1985 Windows has the same rule to dates from 0001-01-01 to 2006-12-31 and according to this rule summer time would start at this exact date (1984-11-04 00:00:00) moving the clock forward 1 hour.
Besides the DST rules for this timezone being wrong, I found some other strange behaviors and inconsistent results from the methods of the TimeZone and TimeZoneInfo classes (GetUtcOffset, IsAmbiguousTime, IsInvalidTime).
As an example (the current timezone of my pc is set to “E. South America Standard Time”):
TimeZone.CurrentTimeZone.GetUtcOffset(new DateTime(1984,11,03,23,00,00, DateTimeKind.Local))
returns -02:00
TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time").GetUtcOffset(new DateTime(1984,11,03,23,00,00, DateTimeKind.Local))
returns -03:00
In the first case it seems that it is using DST rules for the current year and applying them to the 1984 year (In 2015 the summer time will start at 2015-10-18). The second seems to apply the DST rules in Windows for this timezone.
Besides using and storing all dates in UTC is any workaround to avoid these problems? Is really a bug in the way that .NET applies DST rules to a past date where the DST rules where different from those for the current year?
Update After @matt-johnson answer I've done some more tests and found more inconsistent behaviors related to invalid DateTime. As Matt pointed the date in question is an invalid date (according to windows rules). However if run:
var isInvalid = TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time").IsInvalidTime(new DateTime(1984, 11, 4, 0, 0, 0, DateTimeKind.Local))
the result is false, even though by the windows DST rules should be considered invalid. But if run:
var isInvalid2 = TimeZoneInfo.Local.IsInvalidTime(new DateTime(1984, 11, 4, 0, 0, 0, DateTimeKind.Local))
the result is now true. Note that my current TimeZone is “E. South America Standard Time” (TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time").StandardName == TimeZoneInfo.Local.StandardName is true).
Trying to convert the DateTime to UTC using TimeZoneInfo.ConvertTimeToUtc throws an exception as pointed out by Matt
Upvotes: 4
Views: 457
Reputation: 241420
The behavior you found with the TimeZone
class (of using the current rule, rather than the correct applicable rule) is well documented on MSDN:
The
TimeZone
class supports only a single daylight saving time adjustment rule for the local time zone. As a result, theTimeZone
class can accurately report daylight saving time information or convert between UTC and local time only for the period in which the latest adjustment rule is in effect. In contrast, theTimeZoneInfo
class supports multiple adjustment rules, which makes it possible to work with historic time zone data.
You should consider the TimeZone
class deprecated, and only use the TimeZoneInfo
class.
Regarding the conversion mismatch, the error is actually caused when you call ToUniversalTime
on a DateTime
. The value you provided in d
is right at the moment of the spring-forward transition (as far as Windows sees it anyway). That means values from 00:00:00
through 00:59:59.9999999
are invalid on that date. The day starts at 1:00 am, not midnight.
Consider that instead of calling ToUniversalTime
, you might have written the following code:
var dUtc = TimeZoneInfo.ConvertTimeToUtc(d, TimeZoneInfo.Local);
You might think that to be equivalent, but this code throws an exception because the supplied input in d
has been skipped over by the DST transition. This doesn't occur with DateTime.ToUniversalTime
because there's an internal flag passed called TimeZoneInfoOptions.NoThrowOnInvalidTime
, which you can see in the reference sources. Also interesting is that the behavior of NoThrowOnInvalidTime
has changed between .NET 3.5 and .NET 4.0. In your example case it will return 02:00 UTC under .NET 3.5, and 03:00 UTC under .NET 4.x. I'm not sure that I agree with this change, but it is the underlying reason for the roundtrip mismatch.
And finally - as you noted, Brazil's 1984 time zones are not the same as the earliest 2006 time zone data Windows contains. In general, Windows time zones are not a good source for historical information. Instead, you should consider using TZDB time zones, which has history to at least 1970, and earlier in many cases. In .NET, you can do this with the Noda Time library. The equivalent zone would be "America/Sao_Paulo"
.
However, still realize that even with Noda Time, you won't be able to roundtrip an invalid local date/time. If it's not valid in the local time zone, then the conversion from utc to local can never yield that result.
Upvotes: 2