.NET中的时区和夏令时往返服务器

时间:2015-08-20 16:54:11

标签: .net datetime timezone dst

我们的应用程序遇到一个问题,在特定的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所指出的异常

1 个答案:

答案 0 :(得分:2)

您使用TimeZone类(使用当前规则而不是正确的适用规则)找到的行为已有详细记录on MSDN

  

TimeZone类仅支持本地时区的单个夏令时调整规则。因此,TimeZone类可以准确地报告夏令时信息,或仅在最新调整规则生效期间在UTC和本地时间之间进行转换。相比之下,TimeZoneInfo类支持多个调整规则,这使得可以使用历史时区数据。

您应该考虑弃用TimeZone类,并且只使用TimeZoneInfo类。

关于转换不匹配,当您在ToUniversalTime上呼叫DateTime时,实际上会导致错误。您在d中提供的值恰好在Spring-forward转换时(就Windows而言,无论如何)。这意味着该日期00:00:0000: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的转换永远不会产生该结果。