.Net DateTime 与本地时间和 DST

Posted

技术标签:

【中文标题】.Net DateTime 与本地时间和 DST【英文标题】:.Net DateTime with local time and DST 【发布时间】:2015-05-16 05:22:05 【问题描述】:

恐怕我不太了解 .Net 的 DateTime 类如何处理本地时间戳(我住在德国,所以我的语言环境是 de_DE)。也许有人可以启发我一点;-)

DateTime 构造函数可以用年、月等参数调用。此外,还可以提供 DateTimeKindLocalUtcUnspecified(=default)。

例子:

DateTime a = new DateTime(2015, 03, 29, 02, 30, 00, DateTimeKind.Local);
DateTime b = new DateTime(2015, 03, 29, 02, 30, 00, DateTimeKind.Utc);
DateTime c = new DateTime(2015, 03, 29, 02, 30, 00, DateTimeKind.Unspecified);
DateTime d = new DateTime(2015, 03, 29, 02, 30, 00);

根据定义,值 c 和 d 是相同的。但是,如果我将所有内容相互比较,所有四个都是相同的。在 VS 的调试器中检查对象显示Ticks 值(以及InternalTicks)对于所有对象都是相同的。但是,内部dateData 值不同,但显然被比较运算符忽略了。

您可能已经注意到,我为今年 3 月 29 日凌晨 02:30 构建了一个值。这个时间点在我们的时区中不存在,因为它通过切换到夏令时被跳过。所以我原以为构造对象a会得到一个异常,但这并没有发生。

此外,DateTime 有一个方法ToUniversalTime() 将解释为本地时间的值转换为等效的 UTC 值。为了测试,我运行了一个循环如下:

DateTime dt = new DateTime(2015, 03, 29, 01, 58, 00, DateTimeKind.Local);
DateTime dtEnd = new DateTime(2015, 03, 29, 03, 03, 00, DateTimeKind.Local);
while (dt < dtEnd)

    Log(" Localtime " + dt + " converted to UTC is " + dt.ToUniversalTime());
    dt = dt.AddMinutes(1);

结果是:

Localtime 29.03.2015 01:58:00 converted to UTC is 29.03.2015 00:58:00
Localtime 29.03.2015 01:59:00 converted to UTC is 29.03.2015 00:59:00
Localtime 29.03.2015 02:00:00 converted to UTC is 29.03.2015 01:00:00
Localtime 29.03.2015 02:01:00 converted to UTC is 29.03.2015 01:01:00
Localtime 29.03.2015 02:02:00 converted to UTC is 29.03.2015 01:02:00
...
Localtime 29.03.2015 02:58:00 converted to UTC is 29.03.2015 01:58:00
Localtime 29.03.2015 02:59:00 converted to UTC is 29.03.2015 01:59:00
Localtime 29.03.2015 03:00:00 converted to UTC is 29.03.2015 01:00:00
Localtime 29.03.2015 03:01:00 converted to UTC is 29.03.2015 01:01:00
Localtime 29.03.2015 03:02:00 converted to UTC is 29.03.2015 01:02:00

因此,.Net 将不存在的时间戳从本地时间转换为 UTC 没有问题。此外,向现有的本地时间戳添加一分钟不是本地感知的,并且会提供一个不存在的时间戳。

因此,添加 64 分钟后,转换后的 UTC 时间戳仅比之前大 4 分钟。

换句话说,本地时间和UTC之间的转换应该是一个双射,在合法的时间戳值之间给出一一对应的关系。

长话短说:我如何以预期的方式正确处理这个问题(根据.Net)?如果没有正确考虑,拥有DateTimeKind 是什么意思?我什至不敢问如何处理闰秒(23:59:60);-)

【问题讨论】:

这是 Jon Skeet 需要介入并使用 Noda Time 发表论文的地方 Hmya,你为什么要使用一个在实践中从未真正发生过的时间?你在锻炼GIGO,垃圾进垃圾出。不要惊讶你得到垃圾。如果您希望您的代码以可预测的方式运行,请严格使用 UTC,仅在可能的最后一刻转换为本地时间,就在人类看到它之前。 在相关的注释中,Jon 确实在那篇文章中提到闰秒的想法让他哭了。所以你并不孤单。 :) @HansPassant:基础任务是针对一些基于时间的数据创建报告。我的基本方法是迭代民用(即本地)时间/日期,因为这决定了,例如,测量是否属于一个日期或另一个日期。按小时迭代时,会出现我发帖中显示的奇怪效果。目前这只是为了了解 DateTime API 是如何工作的。确实有效。不过我想我还是看看野田时间吧。 @paqogomez 取决于地球的哪个部分在给定时刻获得阳光,可能是 Jon Skeet 或 Matt Johnson,因为他们都是 NodaTime 的主要贡献者。 【参考方案1】:

是的,.NET 中的 DateTime 类型非常混乱,正如您所看到的,因为它不支持时区、多个日历和许多其他有用的概念(例如间隔等)的概念。

更好一点的是DateTimeOffset 类型,它添加了时区偏移信息。 DateTimeOffset 将允许您更准确地表示您在问题中显示的时间,并且比较将考虑时区偏移量。但这种类型也不是完美的。它仍然不支持真正的时区信息,只支持偏移量。因此无法执行复杂的 DST 计算或支持高级日历。

如需更彻底的解决方案,您可以使用NodaTime

【讨论】:

来自 Unix 和 Python,我习惯于拥有一个非常可靠的日期/时间 API,始终在内部使用 UTC 刻度并在通用时间和本地时间之间进行可靠的输入/输出转换。我的问题是评估 .Net 在这方面的可能性的结果......谢谢你,迈克,还有 @paqogomez 给我指点野田时间【参考方案2】:

迈克的回答很好。是的,DateTimeOffset 几乎总是比DateTime 更受欢迎(但不是所有 场景),Noda Time 在许多方面都非常优越。不过,我可以添加更多细节来解决您的问题和意见。

首先,MSDN has this to say:

UTC 时间适用于计算、比较以及将日期和时间存储在文件中。本地时间适合在桌面应用程序的用户界面中显示。时区感知应用程序(例如许多 Web 应用程序)还需要与许多其他时区一起工作。

...

时区之间的转换操作(例如 UTC 和本地时间之间,或一个时区和另一个时区之间)考虑夏令时,但算术和比较操作不考虑。

由此我们可以得出结论,您提供的测试无效,因为它使用本地时间执行计算。它的有用之处仅在于它突出了 API 如何允许您打破其自己的文档化指南。一般来说,由于从 02:00 到 03:00 之前的时间在该日期的当地时区中不存在,因此除非通过数学方法(例如通过每日重复)获得,否则在现实世界中不太可能遇到没有考虑 DST 的模式。

顺便说一句,Noda Time 解决此问题的部分是 ZoneLocalMappingResolver,当通过 localDateTime.InZone 方法将 LocalDateTime 转换为 ZonedDateTime 时使用它。有一些合理的默认值,例如 InZoneStrictlyInZoneLeniently,但它不只是像您用 DateTime 说明的那样默默地移动。

关于你的断言:

换句话说,本地时间和UTC之间的转换应该是一个双射,在合法的时间戳值之间给出一一对应的关系。

实际上,它不是双射。 (the definition of bijection on Wikipedia,它不满足标准 3 或 4。)只有在 UTC 到本地方向的转换是一个函数。本地到 UTC 方向的转换在弹簧向前 DST 转换期间具有不连续性,并且在回退 DST 转换期间具有歧义。您可能希望查看图表in the DST tag wiki。

回答您的具体问题:

我如何以预期的方式正确处理这个问题(根据 .Net)?

DateTime dt = new DateTime(2015, 03, 29, 01, 58, 00, DateTimeKind.Local);
DateTime dtEnd = new DateTime(2015, 03, 29, 03, 03, 00, DateTimeKind.Local);

// I'm putting this here in case you want to work with a different time zone
TimeZoneInfo tz = TimeZoneInfo.Local; // you would change this variable here

// Create DateTimeOffset wrappers so the offset doesn't get lost
DateTimeOffset dto = new DateTimeOffset(dt, tz.GetUtcOffset(dt));
DateTimeOffset dtoEnd = new DateTimeOffset(dtEnd, tz.GetUtcOffset(dtEnd));

// Or, if you're only going to work with the local time zone, you can use
// this constructor, which assumes TimeZoneInfo.Local
//DateTimeOffset dto = new DateTimeOffset(dt);
//DateTimeOffset dtoEnd = new DateTimeOffset(dtEnd);

while (dto < dtoEnd)

    Log(" Localtime " + dto + " converted to UTC is " + dto.ToUniversalTime());

    // Math with DateTimeOffset is safe in instantaneous time,
    // but it might not leave you at the desired offset by local time.
    dto = dto.AddMinutes(1);

    // The offset might have changed in the local zone.
    // Adjust it by either of the following (with identical effect).
    dto = TimeZoneInfo.ConvertTime(dto, tz);
    //dto = dto.ToOffset(tz.GetUtcOffset(dto));

如果没有正确考虑,拥有 DateTimeKind 有什么意义?

最初,DateTime 没有种类。它表现得好像种类未指定。 DateTimeKind 是在 .NET 2.0 中添加的。

它涵盖的主要用例是防止双重转换。例如:

DateTime result = DateTime.UtcNow.ToUniversalTime();

DateTime result = DateTime.Now.ToLocalTime();

在 .NET 2.0 之前,这些都会导致错误数据,因为 ToUniversalTimeToLocalTime 方法必须假设输入值没有转换。它会盲目地应用时区偏移量,即使该值已经在所需的时区中。

还有一些其他的边缘情况,但这是主要的。此外,还有一个隐藏的 第四种 类,用于使以下内容在回退转换期间仍能支持模棱两可的值。

DateTime now = DateTime.Now;
Assert.True(now.ToUniversalTime().ToLocalTime() == now);

Jon Skeet 有 a good blog post about this,您现在也可以在 the .NET Reference sources 或 the new coreclr sources 的 cmets 中看到它的讨论。

我什至不敢问如何处理闰秒(23:59:60);-)

.NET 实际上根本不支持闰秒,包括当前版本的 Noda Time。它们也不受任何 Win32 API 的支持,您也永远不会在 Windows 时钟上观察到闰秒。

在 Windows 中,闰秒通过 NTP 同步应用。时钟滴答作响,就好像闰秒没有发生一样,在下一次时钟同步期间,时间被调整并被吸收。下面是下一个闰秒的样子:

Real World              Windows
--------------------    --------------------
2015-06-30T23:59:58Z    2015-06-30T23:59:58Z
2015-06-30T23:59:59Z    2015-06-30T23:59:59Z
2015-06-30T23:59:60Z    2015-07-01T00:00:00Z   <-- one sec behind
2015-07-01T00:00:00Z    2015-07-01T00:00:01Z
2015-07-01T00:00:01Z    2015-07-01T00:00:02Z   
2015-07-01T00:00:02Z    2015-07-01T00:00:02Z   <-- NTP sync
2015-07-01T00:00:03Z    2015-07-01T00:00:03Z

我在午夜 2 秒后显示同步,但实际上可能要晚得多。时钟同步一直发生,而不仅仅是闰秒。计算机的本地时钟不是一种超精确的仪器——它会漂移,并且必须定期进行校正。你不能假设当前时间总是单调递增的——它可以向前跳,也可以向后跳。

另外,上面的图表并不完全准确。我展示了几秒钟内的硬转变,但实际上操作系统通常会通过在几秒的较长时间内(一次几毫秒)以几个亚秒的增量分散变化的影响来引入微小的修正。

在 API 级别,没有任何 API 将支持超过 59 个秒字段。如果他们 完全支持它,那可能只是在解析期间。

DateTime.Parse("2015-06-30T23:59:60Z")

这将引发异常。如果它工作,它就必须处理额外的闰秒,然后要么返回前一秒 (2015-06-30T23:59:59Z),要么返回下一秒 (2015-07-01T00:00:00Z)。

【讨论】:

非常感谢您富有洞察力和详细的解释。这个话题仍然很困难但很有趣:-)

以上是关于.Net DateTime 与本地时间和 DST的主要内容,如果未能解决你的问题,请参考以下文章

C# - 处理 DST 过渡日的主要时间范围 - 提供的 DateTime 表示无效时间

Rails 使用时区参数和 DST 强制执行 DateTime 偏移

由于 DST 时钟向前,perl DateTime 和不存在的时间用户输入

将 UTC DateTime 转换为另一个时区

为啥只有一个日期受 DST 影响?

PHP \DateTime vs date() DST shift bug?