具有本地时间和 DST 的 .Net DateTime

.Net DateTime with local time and DST

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

DateTime构造函数可以调用年月等参数。此外,可以提供 LocalUtcUnspecified(=默认值)的 DateTimeKind 值。

示例:

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)是如何处理的 ;-)

是的,.NET 中的 DateTime 类型是一团糟,正如您所观察到的,因为它不支持时区、多个日历和许多其他有用的概念,例如时间间隔等。

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

要获得更彻底的解决方案,您可以使用 NodaTime

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

首先,MSDN has this to say

UTC time is suitable for calculations, comparisons, and storing dates and time in files. Local time is appropriate for display in user interfaces of desktop applications. Time zone-aware applications (such as many Web applications) also need to work with a number of other time zones.

...

Conversion operations between time zones (such as between UTC and local time, or between one time zone and another) take daylight saving time into account, but arithmetic and comparison operations do not.

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

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

关于你的断言:

In other words, converting between local time and UTC should be a bijection, giving a one-to-one correspondence between legal timestamp values.

实际上,这不是双射。 (来自 the definition of bijection on Wikipedia, it does not satisfy criteria 3 or 4.) Only conversion in the UTC-to-local direction is a function. Conversion in the local-to-UTC direction has a discontinuity in during the spring-forward DST transition, and has ambiguity during the fall-back DST transition. You may wish to review the graphs in the DST tag wiki.

回答您的具体问题:

How do I handle this correctly the intended way (according to .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));
}

What is the sense of having DateTimeKind if it is not taken into account correctly?

本来DateTime是没有样的。它表现得好像种类是未指定的。 DateTimeKind 已添加到 .NET 2.0 中。

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

DateTime result = DateTime.UtcNow.ToUniversalTime();

DateTime result = DateTime.Now.ToLocalTime();

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

还有一些其他的边缘情况,但这是主要的。此外,还有一个隐藏的 fourth 种类,它的使用使得以下内容在回退转换期间仍会保留不明确的值。

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

Jon Skeet a good blog post about this, and you can also now see it discussed in the comments in the .NET Reference sources or in the new coreclr sources.

I don't even dare to ask how leap seconds (at 23:59:60) are handled ;-)

.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 秒显示同步,但实际上可能会晚很多。时钟同步一直在发生,而不仅仅是闰秒。计算机的本地时钟不是超精密仪器 - 它会漂移,并且必须定期进行校正。您不能假设当前时间总是单调递增 - 它可以向前跳,也可以向后跳。

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

在 API 级别,API 中的 none 将支持超过 59 秒的字段。如果他们 完全支持它,那可能只是在解析期间。

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

这将引发异常。如果它 工作,它必须修改额外的闰秒和 return 前一秒 (2015-06-30T23:59:59Z) 或下一秒 (2015-07-01T00:00:00Z).