关于datetime:.NET中的时区和夏令时往返服务器

Timezones and Daylight saving in .NET round trip to server

我们的应用程序遇到一个问题,在特定的TimeZone中有一个特定日期,在从服务器到客户端,然后从客户端到服务器的往返中,不保留DateTime的值。这在巴西利亚时区("E.南美标准时间")中观察到,并且DateTime值是"1984-11-04 00:00:00"。

我能够使用以下代码重现此问题:

1
2
3
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.南美标准时间"):

1
2
3
4
5
    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中的所有日期之外,还有哪些解决方法可以避免这些问题?
在将DST规则应用于DST规则与当前年度不同的过去日期时,真的是一个错误吗?

更新@ matt-johnson回答之后我做了一些测试,发现了与无效DateTime相关的更多不一致行为。
正如马特指出的那样,有关日期是无效日期(根据Windows规则)。但是如果运行:

1
var isInvalid = TimeZoneInfo.FindSystemTimeZoneById("E. South America Standard Time").IsInvalidTime(new DateTime(1984, 11, 4, 0, 0, 0, DateTimeKind.Local))

结果是假的,即使通过Windows DST规则应被视为无效。但如果运行:

1
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指出的异常


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

The TimeZone class supports only a single daylight saving time adjustment rule for the local time zone. As a result, the TimeZone 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, the TimeZoneInfo class supports multiple adjustment rules, which makes it possible to work with historic time zone data.

您应该考虑不推荐使用TimeZone类,并且只使用TimeZoneInfo类。

关于转换不匹配,当您在DateTime上调用ToUniversalTime时,实际上会导致错误。您在d中提供的值恰好在Spring-forward转换时(就Windows而言,无论如何)。这意味着00:00:0000:59:59.9999999的值在该日期无效。这一天从凌晨1点开始,而不是午夜。

请考虑不是调用ToUniversalTime,而是编写以下代码:

1
var dUtc = TimeZoneInfo.ConvertTimeToUtc(d, TimeZoneInfo.Local);

您可能认为这是等效的,但此代码会引发异常,因为DST转换已跳过d中提供的输入。 DateTime.ToUniversalTime不会发生这种情况,因为传递了一个名为TimeZoneInfoOptions.NoThrowOnInvalidTime的内部标志,您可以在参考源中看到。同样有趣的是,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的转换永远不会产生该结果。