为啥减去两个本地 DateTime 值似乎不能说明夏令时?

Posted

技术标签:

【中文标题】为啥减去两个本地 DateTime 值似乎不能说明夏令时?【英文标题】: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 偏移量确实需要花时间。 【参考方案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 的内置类型则不了解。

给你一些后续阅读:

What's wrong with DateTime anyway? More Fun with DateTime The case against DateTime.Now Five Common Daylight Saving Time Antipatterns of .NET Developers

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

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

以上是关于为啥减去两个本地 DateTime 值似乎不能说明夏令时?的主要内容,如果未能解决你的问题,请参考以下文章

如何从两个选择语句中减去值

减去两个 DateTime 对象 - Python [重复]

为啥 DateTime.Now 和 DateTime.UtcNow 不是我当前的本地日期时间?

Python - 计算两个 datetime.time 对象之间的差异

如何用Python在指定日期上减去7天?

C# - 获取两个日期然后只减去年份[重复]