c# - 在计算 DateTimes 之间的持续时间时安全处理夏令时(或任何其他理论上的非常量偏移)
问题描述
我知道即使在过去的 24 小时内,这也不是第一次提出这个话题,但我很惊讶我还没有遇到一个明确的/最佳实践解决方案来解决这个问题。这个问题似乎也与我认为将所有日期保存为 UTC 的不费吹灰之力的设计决定相矛盾。我将尝试在这里说明问题:
给定两个 DateTime 对象,找出它们之间的持续时间,同时考虑夏令时。
考虑以下场景:
UtcDate - LocalDate,其中 LocalDate 比 DST 切换早 1 毫秒。
LocalDateA - LocalDateB,其中 LocalDateB 比 DST 切换早 1 毫秒。
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 问题是否影响不足以做出错误的决定?
在计算日期之间的差异时,不太可能有人会意识到这种方法,甚至不会考虑这个问题。有没有更安全的解决方案?
如果我们齐心协力,也许科技行业可以说服国会废除夏令时。
解决方案
正如你所指出的,这个问题之前已经讨论过了。这里和这里有两个很好的帖子可供审查。
此外,有关文档DateTime.Subtract
有这样的说法:
该方法在执行减法时
Subtract(DateTime)
不考虑这Kind
两个值的属性值。DateTime
在减去DateTime
对象之前,请确保对象代表同一时区的时间。否则,结果将包括时区之间的差异。笔记
该
DateTimeOffset.Subtract(DateTimeOffset)
方法在执行减法时确实考虑了时区之间的差异。
除了“表示同一时区的时间”之外,请记住,即使对象处于同一时区,减值DateTime
仍然不会考虑 DST 或两个对象之间的其他转换。
关键是要确定经过的时间,您应该减去绝对时间点。这些最好用DateTimeOffset
.NET 中的 a 表示。
如果你已经有了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
扩展方法(我也在其他答案中使用过)。
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));
}
推荐阅读
- python - dict_values 交集和可散列类型
- python - 如何将用户选择的 Tkinter 中列表框行的内容复制到 Windows 剪贴板
- python - 通过 Python 中的树结构产生所有完整的根叶路径
- python - 通过 py_function 的高阶梯度
- r - 使用“networkD3”更改桑基图的颜色字体
- asp.net-core - 有没有办法在 .net core ILogger 接口中记录异常数据字典中的值?
- angular - Angular Service 未执行 switchmap
- c# - 将 PersianCalendar 保存为表中的字符串而不是 DateTime 类型
- hibernate - 为什么 Jpa 在使用 @Fetch(FetchMode.JOIN) 时不发出 JOIN 查询
- webpack - Webpack 4,Slick carousel 寻找 /img 和 /fonts 目录