System.DateTime 实例通过 Unix 时间戳的往返转换延迟了 1 小时

Round-trip conversion of a System.DateTime instance via a Unix time stamp is off by 1 hour

注意:为方便起见,PowerShell 用于演示行为,但问题是关于 System.DateTime .NET type, contrasted with type System.DateTimeOffset.

的令人惊讶的行为

这种行为可能有一个很好的概念原因,但它逃避了我。 如果有,了解为什么以及如何避免这个陷阱会很有帮助。

以下 PowerShell 代码段演示了 DateTime 实例通过其 Unix 时间等价物的本地时间表示的往返转换:

# Get midnight 1 Jul 2018 in local time.
$date = Get-Date '2018-07-01'

# Convert to Unix time (seconds since midnight 1 Jan 1970 UTC)
# Note: In PowerShell Core this command could be simplified to: Get-Date -Uformat %s $date
$unixTime = [int] (Get-Date -Uformat %s $date.ToUniversalTime())

# Reconvert the Unix time stamp to a local [datetime] instance.
# Note that even though the input string is a UTC time, the cast creates
# a *local* System.DateTime instance (.Kind equals Local)
$dateFromUnixTime1 = ([datetime] '1970-01-01Z').AddSeconds($unixTime)

# Reconvert the Unix time stamp to a local [datetime] instance via 
# a [System.DateTimeOffset] instance:
$dateFromUnixTime2 = ([datetimeoffset ] '1970-01-01Z').AddSeconds($unixTime).LocalDateTime

# Output the results
@"
original:                           $date
Unix time:                          $unixTime
reconstructed via [datetime]:       $dateFromUnixTime1
reconstructed via [datetimeoffset]: $dateFromUnixTime2
"@

以上结果(在我的 Eastern Timezone 中的美国英语系统上):

original:                           07/01/2018 00:00:00
Unix time:                          1530417600
reconstructed via [datetime]:       06/30/2018 23:00:00
reconstructed via [datetimeoffset]: 07/01/2018 00:00:00

如您所见,通过([datetime] '1970-01-01Z')实例获得的[datetime]实例-其.Kind值为Local,即本地 日期 - 关闭 1 小时,而基于 [datetimeoffset] 的计算(基于 UTC)按预期工作。

我怀疑这与 DST(夏令时)有关 - 例如 2018-12-01 就不会发生 - 但我不清楚为什么。

最终问题是由于 AddSeconds 在基于本地时间的 DateTime 上被调用。 The .net docs 说(强调我的):

Conversion operations between time zones (such as between UTC and local time, or between one time zone and another) take daylight saving time into account, but arithmetic and comparison operations do not.


我不是 PowerShell 的重度专家,但看来 [datetime] 'somestring' 相当于调用 DateTime.Parse("somestring")。对于 API,默认行为是根据本地时区 return 值。由于您传递了一个 Z,输入被视为 UTC,然后该值被转换为本地时间。这就是差异的原因。

在 C# 中(与 DateTime 保持一致)可以传递参数来控制解析和输出行为:

DateTime.Parse("1970-01-01Z", CultureInfo.InvariantCulture, DateTimeStyles.RoundTripKind)

RoundTripKind 样式(部分)表示输出类型应由输入字符串中的信息确定。由于 Z 表示 UTC,因此您将在输出中获得基于 UTC 的 DateTime

我不确定如何将这些参数传递到 powershell 中的 shorthand(类型加速器?),但一般来说是这样的:

[datetime]::Parse('1970-01-01Z', [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::RoundtripKind)

此外,您可以使用内置方法而不是解析来简化操作:

DateTimeOffset.FromUnixTimeSeconds(unixTime)

如果你愿意,你可以得到一个 DateTime 折扣(但请记住 DateTimeOffset.UtcDateTime 保留 UTC 类型,而 DateTimeOffset.DateTime 将始终具有未指定的类型,而 DateTimeOffset.LocalDateTime return是当地的那种)。

我想的powershell应该是这样的:

[datetimeoffset]::FromUnixTimeSeconds($unixTime).UtcDateTime