.Net DateTime与当地时间和夏令时

Ste*_*fan 4 .net c# datetime localtime dst

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

DateTime构造可以用一年被调用,一个月等参数.另外,可以提供DateTimeKind值,,或(=默认值).LocalUtcUnspecified

例:

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);
Run Code Online (Sandbox Code Playgroud)

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

您可能已经注意到,我在今年3月29日02:30 AM构建了一个值.这个时刻在我们的时区中不存在,因为切换到夏令时会跳过它.所以我原本期望得到构造对象的异常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);
}
Run Code Online (Sandbox Code Playgroud)

结果是:

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
Run Code Online (Sandbox Code Playgroud)

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

因此,在转换后,添加64个单分钟会产生比以前大4分钟的UTC时间戳.

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

简而言之:如何正确处理预期的方式(根据.Net)?DateTimeKind如果没有正确考虑,有什么感觉?我甚至不敢问如何处理闰秒(23:59:60);-)

Mik*_*scu 5

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

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

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


Mat*_*int 5

迈克的回答很好.是的,DateTimeOffset几乎总是被优先考虑DateTime(但不适用于所有情况),并且Noda Time在许多方面都非常优越.但是,我可以添加更多细节来解决您的问题和观察.

首先,MSDN有这样的说法:

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

...

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

由此我们可以得出结论,您提供的测试无效,因为它使用本地时间执行计算.它之所以有用,是因为它突出了API如何允许您打破自己的文档指南.通常,由于在该日期当地时区中从02:00到03:00之前的时间不存在,除非是在数学上获得,否则在现实世界中不可能遇到,例如每天复发没有考虑DST的模式.

顺便说一句,Noda Time解决这个问题的部分是在通过该方法ZoneLocalMappingResolver转换LocalDateTime为a时ZonedDateTime使用的部分localDateTime.InZone.有一些合理的默认值,例如InZoneStrictly,或者InZoneLeniently,它不仅仅是像你所说的那样默默地移动DateTime.

关于你的断言:

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

实际上,这不是一个双射.(根据维基百科上的双射定义,它不满足标准3或4.)只有UTC到本地方向的转换才是一个函数.在本地到UTC方向的转换在弹簧前向DST转换期间具有不连续性,并且在后退DST转换期间具有模糊性.您可能希望查看DST标记维基中的图表.

回答您的具体问题:

如何正确处理预期的方式(根据.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));
}
Run Code Online (Sandbox Code Playgroud)

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

原来,DateTime没有一种.它表现得好像没有指定那种. DateTimeKind在.NET 2.0中添加了.

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

DateTime result = DateTime.UtcNow.ToUniversalTime();
Run Code Online (Sandbox Code Playgroud)

要么

DateTime result = DateTime.Now.ToLocalTime();
Run Code Online (Sandbox Code Playgroud)

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

还有一些其他边缘情况,但这是主要情况.此外,存在隐藏的第四种类型,其使用使得在回退转换期间以下仍将保持具有模糊值.

DateTime now = DateTime.Now;
Assert.True(now.ToUniversalTime().ToLocalTime() == now);
Run Code Online (Sandbox Code Playgroud)

Jon Skeet有一篇关于此的博文,你现在也可以在.NET Reference源代码新的coreclr源代码中看到它.

我甚至不敢问如何处理闰秒(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
Run Code Online (Sandbox Code Playgroud)

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

此外,上面的图表并不完全准确.我在几秒钟内表现出了硬转换,但实际上OS通常会通过在几秒钟的较长时间内(一次几毫秒)分散几个亚秒增量的变化效果来引入微小的修正.

在API级别,所有API都不会在秒字段中支持超过59个.如果他们完全支持它,它可能只是在解析期间.

DateTime.Parse("2015-06-30T23:59:60Z")
Run Code Online (Sandbox Code Playgroud)

这将抛出异常.如果工作,那就得第二Munge时间额外的飞跃,要么返回先前的第二(2015-06-30T23:59:59Z),或在下一秒(2015-07-01T00:00:00Z).