在计算 DateTimes 之间的持续时间时安全处理夏令时(或任何其他理论上的非常量偏移)
Posted
技术标签:
【中文标题】在计算 DateTimes 之间的持续时间时安全处理夏令时(或任何其他理论上的非常量偏移)【英文标题】:Safe handling of daylight savings (or any other theoretical non-constant offset) while calculating durations between DateTimes 【发布时间】:2020-05-07 20:56:35 【问题描述】:我知道即使在过去的 24 小时内,这也不是第一次提出这个话题,但我很惊讶我还没有找到一个明确/最佳实践的解决方案来解决这个问题。这个问题似乎也与我认为将所有日期保存为 UTC 的不费吹灰之力的设计决定相矛盾。我将尝试在这里陈述问题:
给定两个 DateTime 对象,找出它们之间的持续时间,同时考虑夏令时。
考虑以下场景:
UtcDate - LocalDate 其中 LocalDate 比 a 早 1 毫秒 夏令时切换。
LocalDateA - LocalDateB,其中 LocalDateB 为 1 比 DST 切换早几毫秒。
UtcDate - LocalDate.ToUtc() 提供不考虑 DST 开关的持续时间。 LocalDateA.ToUtc() - LocalDateB.ToUtc() 是正确的,但 LocalDateA - LocalDateB 也忽略 DST。
现在,显然有个解决方案可以解决这个问题。我现在使用的解决方案是这种扩展方法:
public static TimeSpan Subtract(this DateTime minuend, TimeZoneInfo minuendTimeZone,
DateTime subtrahend, TimeZoneInfo subtrahendTimeZone)
return TimeZoneInfo.ConvertTimeToUtc(DateTime.SpecifyKind(minuend,
DateTimeKind.Unspecified), minuendTimeZone)
.Subtract(TimeZoneInfo.ConvertTimeToUtc(DateTime.SpecifyKind(subtrahend,
DateTimeKind.Unspecified), subtrahendTimeZone));
我猜它有效。不过我有一些问题:
如果日期在保存之前全部转换为 UTC,那么这个 方法无济于事。时区信息(以及任何处理 DST) 丢失。我已经习惯于始终以 UTC 保存日期,是 DST 问题的影响力不足以使其变得糟糕 决定?
不太可能有人知道这种方法,甚至 考虑到这个问题,在计算两者之间的差异时 日期。有没有更安全的解决方案?
如果我们齐心协力,也许科技行业可以说服 国会废除夏令时。
【问题讨论】:
如果您使用NodaTime,则很难弄错,因为如果没有涉及明确意图的转换,就不可能混合 UTC 和本地。 Jon Skeet 的一些教育读物 - codeblog.jonskeet.uk/2019/03/27/… ......基本上解释了你不能真正计算时间之间的差异,特别是在未来 100% 正确的情况下,因为规则可能会改变。所以你永远不知道“我每天早上 9 点喝咖啡,我喝咖啡休息多长时间”是否意味着 23,24 或 25 小时或两者之间的任何时间(转换为 UTC 会使情况变得更糟):( 正如您所指出的,这已经讨论了 很多次 次。请查看链接的副本。也是in the remarks of these docs。基本上,改用DateTimeOffset
。
是的,DateTimeOffset
有一个固定的偏移量,但这意味着即使两个偏移量不同,减法也能正常工作(在减法之前将它们标准化为 UTC)。是的,我看到您的代码正在改变种类。这是一个问题。考虑subtrahend
是否为DateTimeKind.Utc
,minuend
是否为DateTimeKind.Local
(并且本地时区不是UTC)。您不会获得实际经过时间的差异,而只会获得本地时钟时间与您的代码的差异。 (它甚至可能是负面的。)
我重新打开了您的问题,以便在下面添加更详细的答案。谢谢。
【参考方案1】:
正如你所指出的,这个问题之前已经讨论过了。 Here 和 here 是两个值得审查的好帖子。
另外,DateTime.Subtract
上的the documentation 有这样的说法:
Subtract(DateTime)
方法在执行减法时不考虑两个DateTime
值的Kind
属性的值。在减去DateTime
对象之前,请确保对象代表同一时区的时间。否则,结果将包括时区之间的差异。注意
DateTimeOffset.Subtract(DateTimeOffset)
方法确实在执行减法时考虑了时区之间的差异。
除了“表示同一时区的时间”之外,请记住,即使对象处于同一时区,DateTime
值的减法仍然不会考虑 DST 或两个对象之间的其他转换。
关键是要确定经过的时间,您应该减去绝对时间点。这些最好用 .NET 中的 DateTimeOffset
表示。
如果您已经有 DateTimeOffset
值,则可以减去它们。但是,您仍然可以使用 DateTime
值,只要您先正确地将它们转换为 DateTimeOffset
。
或者,您可以将所有内容都转换为 UTC - 但无论如何您都必须通过 DateTimeOffset
或类似代码才能正确执行此操作。
在您的情况下,您可以将代码更改为以下内容:
public static TimeSpan Subtract(this DateTime minuend, TimeZoneInfo minuendTimeZone,
DateTime subtrahend, TimeZoneInfo subtrahendTimeZone)
return minuend.ToDateTimeOffset(minuendTimeZone) -
subtrahend.ToDateTimeOffset(subtrahendTimeZone);
您还需要 ToDateTimeOffset
扩展方法 (which I've also used on other answers)。
public static DateTimeOffset ToDateTimeOffset(this DateTime dt, TimeZoneInfo tz)
if (dt.Kind != DateTimeKind.Unspecified)
// Handle UTC or Local kinds (regular and hidden 4th kind)
DateTimeOffset dto = new DateTimeOffset(dt.ToUniversalTime(), TimeSpan.Zero);
return TimeZoneInfo.ConvertTime(dto, tz);
if (tz.IsAmbiguousTime(dt))
// Prefer the daylight offset, because it comes first sequentially (1:30 ET becomes 1:30 EDT)
TimeSpan[] offsets = tz.GetAmbiguousTimeOffsets(dt);
TimeSpan offset = offsets[0] > offsets[1] ? offsets[0] : offsets[1];
return new DateTimeOffset(dt, offset);
if (tz.IsInvalidTime(dt))
// Advance by the gap, and return with the daylight offset (2:30 ET becomes 3:30 EDT)
TimeSpan[] offsets = tz.GetUtcOffset(dt.AddDays(-1)), tz.GetUtcOffset(dt.AddDays(1)) ;
TimeSpan gap = offsets[1] - offsets[0];
return new DateTimeOffset(dt.Add(gap), offsets[1]);
// Simple case
return new DateTimeOffset(dt, tz.GetUtcOffset(dt));
【讨论】:
因为您要删除该类型,因此必须提前假定该时区的时间已经正确。例如,如果minuend
已经是 UTC,但 minuendTimeZone
来自 TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time")
或其他时区,那么 ConvertTimeToUtc
执行的转换将不正确。 Kind
属性可以防止这种情况发生,至少对于 UTC 和本地时区而言。它在其他时区表现不佳,这就是 DateTimeOffset
效果更好的原因。以上是关于在计算 DateTimes 之间的持续时间时安全处理夏令时(或任何其他理论上的非常量偏移)的主要内容,如果未能解决你的问题,请参考以下文章