为什么不减去两个本地DateTime值似乎占夏令时?

我正在玩一些C#代码,试图了解如何在夏令时中减去C#中的DateTime对象。

根据谷歌和其他消息来源,2017年东部标准时区的夏令时“春晚”活动是在3月12日凌晨2点。因此,当天的前几个小时是:

12:00am - 1:00am 1:00am - 2:00am (There was no 2:00am - 3:00am hour due to the "spring ahead") 3:00am - 4:00am 

所以,如果我要计算该日期在该时区的凌晨1点到凌晨4点之间的时间差,我希望结果是2小时。

但是,我试图模拟这个问题的代码是返回3小时TimeSpan。

码:

 TimeZoneInfo easternStandardTime = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); DateTime oneAm = TimeZoneInfo.ConvertTime(new DateTime(2017, 03, 12, 01, 00, 00), easternStandardTime); DateTime fourAm = TimeZoneInfo.ConvertTime(new DateTime(2017, 03, 12, 04, 00, 00), easternStandardTime); TimeSpan difference = (fourAm - oneAm); Console.WriteLine(oneAm); Console.WriteLine(fourAm); Console.WriteLine(TimeZoneInfo.Local.IsDaylightSavingTime(oneAm)); Console.WriteLine(TimeZoneInfo.Local.IsDaylightSavingTime(fourAm)); Console.WriteLine(difference); 

在我的电脑上,这会产生:

 2017-03-12 01:00:00.000 -5 2017-03-12 04:00:00.000 -4 False True 03:00:00 

所有这些输出都是预期的 – 除了3小时的最终值,如上所述,我预计将是2小时。

显然,我的代码没有正确地模拟我想到的情况。 缺陷是什么?

注意:

 // These are just plain unspecified DateTimes DateTime dtOneAm = new DateTime(2017, 03, 12, 01, 00, 00); DateTime dtFourAm = new DateTime(2017, 03, 12, 04, 00, 00); // The difference is not going to do anything other than 4-1=3 TimeSpan difference1 = dtFourAm - dtOneAm; // ... but we have a time zone to consider! TimeZoneInfo eastern = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); // Use that time zone to get DateTimeOffset values. // The GetUtcOffset method has what we need. DateTimeOffset dtoOneAmEastern = new DateTimeOffset(dtOneAm, eastern.GetUtcOffset(dtOneAm)); DateTimeOffset dtoFourAmEastern = new DateTimeOffset(dtFourAm, eastern.GetUtcOffset(dtFourAm)); // Subtracting these will take the offset into account! // It essentially does this: [4-(-4)]-[1-(-5)] = 8-6 = 2 TimeSpan difference2 = dtoFourAmEastern - dtoOneAmEastern; // Let's see the results Console.WriteLine("dtOneAm: {0:o} (Kind: {1})", dtOneAm, dtOneAm.Kind); Console.WriteLine("dtFourAm: {0:o} (Kind: {1})", dtFourAm, dtOneAm.Kind); Console.WriteLine("difference1: {0}", difference1); Console.WriteLine("dtoOneAmEastern: {0:o})", dtoOneAmEastern); Console.WriteLine("dtoFourAmEastern: {0:o})", dtoFourAmEastern); Console.WriteLine("difference2: {0}", difference2); 

结果:

 dtOneAm: 2017-03-12T01:00:00.0000000 (Kind: Unspecified) dtFourAm: 2017-03-12T04:00:00.0000000 (Kind: Unspecified) difference1: 03:00:00 dtoOneAmEastern: 2017-03-12T01:00:00.0000000-05:00) dtoFourAmEastern: 2017-03-12T04:00:00.0000000-04:00) difference2: 02:00:00 

请注意, DateTime在其Kind属性中携带DateTimeKind ,默认情况下为Unspecified 。 它不属于任何特定时区。 DateTimeOffset 没有类型,它有一个Offset ,它告诉你本地时间偏离UTC的距离。 这些都没有给你时区。 这就是TimeZoneInfo对象正在做的事情。 请参阅timezone标记wiki中的 “time zone!= offset”。

我认为你可能感到沮丧的部分是,由于几个历史原因, DateTime对象在进行数学运算时并不理解时区,即使你可能有DateTimeKind.Local可以实现它来观察本地时区的转换,但是没有这样做。

您也可能对Noda Time感兴趣,它以更加明智和有目的的方式为.NET中的日期和时间提供了一个非常不同的API。

 using NodaTime; ... // Start with just the local values. // They are local to *somewhere*, who knows where? We didn't say. LocalDateTime ldtOneAm = new LocalDateTime(2017, 3, 12, 1, 0, 0); LocalDateTime ldtFourAm = new LocalDateTime(2017, 3, 12, 4, 0, 0); // The following won't compile, because LocalDateTime does not reference // a linear time scale! // Duration difference = ldtFourAm - ldtOneAm; // We can get the 3 hour period, but what does that really tell us? Period period = Period.Between(ldtOneAm, ldtFourAm, PeriodUnits.Hours); // But now lets introduce a time zone DateTimeZone eastern = DateTimeZoneProviders.Tzdb["America/New_York"]; // And apply the zone to our local values. // We'll choose to be lenient about DST gaps & overlaps. ZonedDateTime zdtOneAmEastern = ldtOneAm.InZoneLeniently(eastern); ZonedDateTime zdtFourAmEastern = ldtFourAm.InZoneLeniently(eastern); // Now we can get the difference as an exact elapsed amount of time Duration difference = zdtFourAmEastern - zdtOneAmEastern; // Dump the output Console.WriteLine("ldtOneAm: {0}", ldtOneAm); Console.WriteLine("ldtFourAm: {0}", ldtFourAm); Console.WriteLine("period: {0}", period); Console.WriteLine("zdtOneAmEastern: {0}", zdtOneAmEastern); Console.WriteLine("zdtFourAmEastern: {0}", zdtFourAmEastern); Console.WriteLine("difference: {0}", difference); 
 ldtOneAm: 3/12/2017 1:00:00 AM ldtFourAm: 3/12/2017 4:00:00 AM period: PT3H zdtOneAmEastern: 2017-03-12T01:00:00 America/New_York (-05) zdtFourAmEastern: 2017-03-12T04:00:00 America/New_York (-04) difference: 0:02:00:00 

我们可以看到三个小时的时间段,但它并不意味着与经过的时间相同。 它只是意味着两个本地值在时钟上相隔三个小时。 NodaTime理解这些概念之间的区别,而.Net的内置类型则不然。

一些后续阅读:

  • DateTime有什么问题?
  • 使用DateTime更有趣
  • 针对DateTime.Now的案例
  • .NET开发人员的五种常见夏令时反模式

哦,还有一件事。 你的代码有这个……

 DateTime oneAm = TimeZoneInfo.ConvertTime(new DateTime(2017, 03, 12, 01, 00, 00), easternStandardTime); 

由于您创建的DateTime具有未指定的类型,因此您要求从计算机的本地时区转换为东部时间。 如果你碰巧不在东部时间,你的oneAm变量可能根本不是凌晨1点!

因此,这在MSDN文档中得到了解决。

基本上,当从另一个中减去一个日期时,你应该使用DateTimeOffset.Subtract()而不是像在这里那样使用算术减法。

 TimeSpan difference = fourAm.Subtract(oneAm); 

产生预期的2小时时差。

好的,所以我对你的代码做了一些小改动。 不确定这是否是你想要实现的目标,但这会给你你想要的东西……

 static void Main() { TimeZoneInfo easternStandardTime = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); TimeZone timeZone = TimeZone.CurrentTimeZone; DateTime oneAm = TimeZoneInfo.ConvertTime(new DateTime(2017, 03, 12, 01, 00, 00), easternStandardTime); DateTime fourAm = TimeZoneInfo.ConvertTime(new DateTime(2017, 03, 12, 04, 00, 00), easternStandardTime); DaylightTime time = timeZone.GetDaylightChanges(fourAm.Year); TimeSpan difference = ((fourAm - time.Delta) - oneAm); Console.WriteLine(oneAm); Console.WriteLine(fourAm); Console.WriteLine(TimeZoneInfo.Local.IsDaylightSavingTime(oneAm)); Console.WriteLine(TimeZoneInfo.Local.IsDaylightSavingTime(fourAm)); Console.WriteLine(difference); Console.ReadLine(); }