.netdatetime.net-coredatetimeoffsetdate-math

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


Note: For convenience, PowerShell is used to demonstrate the behavior, but the question is about surprising behavior of the System.DateTime .NET type, contrasted with type System.DateTimeOffset.

There may be a good conceptual reason for this behavior, but it escapes me. If there is, it would be helpful to understand why and how to avoid this pitfall.

The following PowerShell snippet demonstrates round-trip conversion of a DateTime instance expressed in local time via its Unix time equivalent:

# 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
"@

The above yields (on my US-English system in the 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

As you can see, the [datetime] instance obtained via the ([datetime] '1970-01-01Z') instance - whose .Kind value is Local, i.e., a local date - is off by 1 hour, whereas the [datetimeoffset]-based calculation (which is UTC-based) works as expected.

I suspect that this is related to DST (daylight-saving time) - it wouldn't happen with 2018-12-01, for instance - but I'm unclear why.


Solution

  • Ultimately the problem was due to AddSeconds being called on a local-time based DateTime. The .net docs say (emphasis mine):

    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.


    I'm not a heavy PowerShell expert, but it appears that [datetime] 'somestring' is equivalent to calling DateTime.Parse("somestring"). With that API, the default behavior is to return values in terms of the local time zone. Since you passed a Z, the input is treated as UTC, and then the value is converted to local time. That's the cause of the discrepancy.

    In C# (staying with DateTime) one would pass arguments to control the parsing and output behavior:

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

    The RoundTripKind style says (in part) that the output kind should be determined by the information in the input string. Since Z means UTC, you'll get a UTC-based DateTime in the output.

    I'm not sure offhand how to pass these parameters into the shorthand (type accelerator?) in powershell, but the longhand is like this:

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

    Also, you could just make things easier by using the built-in method instead of parsing:

    DateTimeOffset.FromUnixTimeSeconds(unixTime)
    

    You can get a DateTime off that if you like (though keep in mind that DateTimeOffset.UtcDateTime retains the UTC kind while DateTimeOffset.DateTime will always have unspecified kind, whereas DateTimeOffset.LocalDateTime returns the local kind).

    The powershell I think would be like this:

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