在计算 DateTimes 之间的持续时间时安全处理夏令时(或任何其他理论上的非常量偏移量)
Safe handling of daylight savings (or any other theoretical non-constant offset) while calculating durations between DateTimes
我知道这不是在过去 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,那么这个
方法无济于事。时区信息(以及任何处理
夏令时)丢失。我习惯于始终以 UTC 格式保存日期,是
DST 的问题还不足以使它成为一个糟糕的问题
决定?
这种方法不太可能有人知道,甚至
思考这个问题,在计算两者之间的差异时
日期。有没有更安全的解决方案?
如果我们一起努力,也许科技行业可以说服
国会废除夏令时。
正如您所指出的,之前已经讨论过这个问题。 and 是两个值得评论的好帖子。
另外,DateTime.Subtract
上的 the documentation 有这样的话:
The Subtract(DateTime)
method does not consider the value of the Kind
property of the two DateTime
values when performing the subtraction. Before subtracting DateTime
objects, ensure that the objects represent times in the same time zone. Otherwise, the result will include the difference between time zones.
Note
The DateTimeOffset.Subtract(DateTimeOffset)
method does consider the difference between time zones when performing the subtraction.
除了 "represent times in the same time zone",请记住,即使对象处于同一时区,减去 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));
}
我知道这不是在过去 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,那么这个 方法无济于事。时区信息(以及任何处理 夏令时)丢失。我习惯于始终以 UTC 格式保存日期,是 DST 的问题还不足以使它成为一个糟糕的问题 决定?
这种方法不太可能有人知道,甚至 思考这个问题,在计算两者之间的差异时 日期。有没有更安全的解决方案?
如果我们一起努力,也许科技行业可以说服 国会废除夏令时。
正如您所指出的,之前已经讨论过这个问题。
另外,DateTime.Subtract
上的 the documentation 有这样的话:
The
Subtract(DateTime)
method does not consider the value of theKind
property of the twoDateTime
values when performing the subtraction. Before subtractingDateTime
objects, ensure that the objects represent times in the same time zone. Otherwise, the result will include the difference between time zones.Note
The
DateTimeOffset.Subtract(DateTimeOffset)
method does consider the difference between time zones when performing the subtraction.
除了 "represent times in the same time zone",请记住,即使对象处于同一时区,减去 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));
}