【问题标题】:Why doesn't subtracting two local DateTime values appear to account for Daylight Saving Time?为什么减去两个本地 DateTime 值似乎不能说明夏令时?
【发布时间】:2017-09-24 10:48:13
【问题描述】:

我正在使用一些 C# 代码来尝试了解 C# 中的减法 DateTime 对象在夏令时方面的工作原理。

根据 Google 和其他来源,2017 年东部标准时区的夏令时“提前”事件发生在 3 月 12 日凌晨 2:00。因此,当天的前几个小时是:

   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:00 和凌晨 4:00 之间的时差,我希望结果为 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 小时。

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

【问题讨论】:

  • 如果您在第二天运行它会发生什么(而不是跨越时间变化)?
  • @leigero 结果 TimeSpan 仍然是 3 小时。两个 IsDaylightSavingTime 调用均返回 True。两个日期的时区偏移量显示为 -4。
  • DateTime 不包含任何时区信息。使用DateTimeOffset
  • @SamAxe 我怀疑你可能是对的,但你能进一步解释(也许在这个问题的答案中)?如果 DateTime 不包含任何时区信息,那么采用时区并返回 DateTime 的 TimeZoneInfo.ConvertTime 方法是怎么回事?
  • DateTime.ToString() 允许很慢,字符串是供人类使用的,因此计算 UTC 偏移量确实需要花时间。

标签: c# datetime dst


【解决方案1】:

观察:

// 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 对象正在做的事情。请参阅the timezone tag wiki 中的“时区!= 偏移量”。

我认为您可能感到沮丧的部分是,由于几个历史原因,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 oneAm = TimeZoneInfo.ConvertTime(new DateTime(2017, 03, 12, 01, 00, 00), easternStandardTime);

由于您创建的DateTime 具有未指定的种类,您要求将从您计算机的本地时区转换为东部时间。如果您恰好不是在东部时间,那么您的 oneAm 变量可能根本就不是 1 AM!

【讨论】:

    【解决方案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();
        }
    

    【讨论】:

    • 谢谢。你能解释一下为什么这是必要的吗?我仍然对为什么从 2017-03-12 04:00 -4(即 8:00 UTC)减去 2017-03-12 01:00 -5(即 6:00 UTC)产生 3 感到困惑。
    • 据我了解,该语言可以理解它实际上是在时间变化的时候,但是当你做时间跨度时,它没有被考虑在内,看起来就像你在12/03/2017 01.00.00 和 12/03/2017 04.00.00。又名 4 -1。我发现这篇文章有助于解释如何理解夏令时。它在底部:msdn.microsoft.com/en-us/library/ms973825.aspx
    • 这仍然是个问题,因为在转换函数中引入了本地时区。您正在将本地时间转换为东部时间。这与应用东部时区偏移不同。此外,这里的 DaylightTime.Delta 对象只需 1 小时。它不会做你认为它会做的事情。真的,GetDaylightChanges 方法很少有用。
    【解决方案3】:

    所以这在MSDN 文档中得到了解决。

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

    TimeSpan difference = fourAm.Subtract(oneAm);
    

    产生预期的 2 小时时差。

    【讨论】:

    • 在发布之前,我确实尝试过使用 .Subtract 方法而不是减号运算符。但是,我使用的是 DateTime 对象,而不是 DateTimeOffset,并且代码仍然返回 3 小时的 TimeSpan。我开始怀疑在我的示例中尝试使用 DateTime 对象进行数学运算不是正确的方法,尽管我还不太明白为什么会这样。
    • 仍然产生与那里相同的 3 小时。
    • 您能解释一下为什么使用 DateTime 不起作用吗?我仍然对为什么从 2017-03-12 04:00 -4(即 8:00 UTC)中减去 2017-03-12 01:00 -5(即 6:00 UTC)产生 3 感到困惑。
    • 首先,fourAm.Subtract(oneAm) 调用DateTime.Subtract,而不是DateTimeOffset.Subtract。其次,您链接到的文档说明相反:它明确指出,在支持运算符重载的语言中,您可以只使用 - 而不是调用 Subtract
    猜你喜欢
    • 2010-10-22
    • 2015-10-24
    • 2019-03-26
    • 2023-04-06
    • 1970-01-01
    • 2011-08-29
    • 2022-12-11
    • 1970-01-01
    • 2016-01-03
    相关资源
    最近更新 更多