我们的应用程序遇到一个问题,在特定的TimeZone中有一个特定的日期,在从服务器到客户端,然后从客户端到服务器的往返中,没有保留DateTime的值。这在巴西利亚时区(“E。South America Standard Time”)观察到,DateTime值为“1984-11-04 00:00:00”。
我能够使用以下代码重现此问题:
DateTime d = new DateTime(1984, 11, 4, 0, 0, 0, DateTimeKind.Local);
var dUtc = d.ToUniversalTime();
var dRtLocal = dUtc.ToLocalTime();
dUTC的最终值是“1984-11-04 03:00:00”(正确),而dRtLocal是“1984-11-04 01:00:00”(不太正确)。
我发现虽然巴西的夏令时仅在1985年开始,但Windows的规则与0001-01-01至2006-12-31的日期相同,根据此规则夏季时间将从此确切日期开始( 1984-11-04 00:00:00)将时钟向前移动1小时。
除了这个时区的DST规则错误之外,我还发现了TimeZone和TimeZoneInfo类(GetUtcOffset,IsAmbiguousTime,IsInvalidTime)方法的一些其他奇怪的行为和不一致的结果。
作为示例(我的电脑的当前时区设置为“E.南美标准时间”):
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
在第一种情况下,它似乎正在使用本年度的DST规则并将其应用于1984年(2015年夏季时间将从2015-10-18开始)。第二个似乎在Windows中应用此时区的DST规则。
除了使用和存储UTC中的所有日期之外,还有哪些解决方法可以避免这些问题? 是否真的是.NET将DST规则应用于DST规则与当前年度不同的过去日期的错误?
更新在@ matt-johnson回答之后我已经做了一些测试,发现了与无效DateTime相关的更多不一致行为。 正如马特指出的那样,有关日期是无效日期(根据Windows规则)。但是如果运行:
var isInvalid = TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time").IsInvalidTime(new DateTime(1984, 11, 4, 0, 0, 0, DateTimeKind.Local))
结果为false,即使Windows DST规则应被视为无效。但如果运行:
var isInvalid2 = TimeZoneInfo.Local.IsInvalidTime(new DateTime(1984, 11, 4, 0, 0, 0, DateTimeKind.Local))
结果现在是真的。请注意,我当前的TimeZone是“E。南美标准时间“(TimeZoneInfo.FindSystemTimeZoneById(" E.南美标准时间")。StandardName == TimeZoneInfo.Local.StandardName为true)。
尝试使用TimeZoneInfo.ConvertTimeToUtc将DateTime转换为UTC会抛出Matt所指出的异常
答案 0 :(得分:2)
您使用TimeZone
类(使用当前规则而不是正确的适用规则)找到的行为已有详细记录on MSDN:
TimeZone
类仅支持本地时区的单个夏令时调整规则。因此,TimeZone
类可以准确地报告夏令时信息,或仅在最新调整规则生效期间在UTC和本地时间之间进行转换。相比之下,TimeZoneInfo
类支持多个调整规则,这使得可以使用历史时区数据。
您应该考虑弃用TimeZone
类,并且只使用TimeZoneInfo
类。
关于转换不匹配,当您在ToUniversalTime
上呼叫DateTime
时,实际上会导致错误。您在d
中提供的值恰好在Spring-forward转换时(就Windows而言,无论如何)。这意味着该日期00:00:00
到00:59:59.9999999
的值无效。这一天从凌晨1点开始,而不是午夜。
考虑到您可能编写了以下代码而不是调用ToUniversalTime
:
var dUtc = TimeZoneInfo.ConvertTimeToUtc(d, TimeZoneInfo.Local);
您可能认为这是等效的,但是此代码会引发异常,因为DST转换已跳过d
中提供的输入。这个不会与DateTime.ToUniversalTime
一起发生,因为传递了一个名为TimeZoneInfoOptions.NoThrowOnInvalidTime
的内部标记,您可以看到in the reference sources。同样有趣的是,NoThrowOnInvalidTime
的行为在.NET 3.5和.NET 4.0之间发生了变化。在您的示例中,它将在.NET 3.5下返回02:00 UTC,在.NET 4.x下返回03:00 UTC。我不确定我是否同意这种变化,但这是导致往返不匹配的根本原因。
最后 - 正如您所指出的那样,巴西的1984年时区与Windows包含的最早的2006年时区数据不同。通常,Windows时区不是历史信息的良好来源。相反,您应该考虑使用TZDB时区,其历史至少为1970年,在许多情况下更早。在.NET中,您可以使用Noda Time库执行此操作。等效区域为"America/Sao_Paulo"
。
然而,仍然意识到即使使用Noda Time,您也无法绕过无效的本地日期/时间。如果它在本地时区无效,则从utc到local的转换永远不会产生该结果。