Search code examples
powershelldatetimedst

How can I account for summer time when adding hours to a DateTime base value in PowerShell?


I'm parsing the dnsRecord attribute from a CSVDE dump of DNS records.

The attribute stores the timestamp as the number of hours that have elapsed since midnight (00:00:00), January 1, 1601 UTC in a string of hex digits.

So far, I have (with hard-coded sample value):

$dnsRecordTimestampBytes = '0038a293'
(Get-Date '01/01/1601Z').AddHours([int]('0x' + $dnsRecordTimestampBytes))

which results in 03 June 2024 11:00:00 but it should be 03 June 2024 12:00:00.

When I parse a value which is not in Summer Time, it gives the correct value, e.g.

$dnsRecordTimestampBytes = '00389c04'
(Get-Date '01/01/1601Z').AddHours([int]('0x' + $dnsRecordTimestampBytes))

gives 25 March 2024 12:00:00 which is correct.

Is there a way to fix this without me having to do bounds-checks on the start and end of summer time?


Solution

  • To get the correct result, you must convert your timestamp from UTC to local time only after adding the hours, and the problem is that your Get-Date call - despite being passed a UTC timestamp string as an argument - returns a local timestamp (of type [datetime]):

    (Get-Date '01/01/1601Z').Kind # -> Local
    

    The immediate fix is to call .ToUniversalTime() first, then add the hours, and then call .ToLocalTime():

    (Get-Date '1601-01-01Z').    # better: ([datetime] '1601-01-01Z').
      ToUniversalTime().
      AddHours([int]('0x' + $dnsRecordTimestampBytes)).
      ToLocalTime()
    

    A more concise solution is to use the [datetimeoffset] class:

    ([datetimeoffset] '1601-01-01Z').
      AddHours([int]('0x' + $dnsRecordTimestampBytes)).
      LocalDateTime
    

    As an aside:

    • Note that I've reformatted your timestamp string from 01/01/1601Z to 1601-01-01Z so as to use an unambiguous format (though in this particular case, with both the month and day being 01, this isn't strictly necessary).

    • Either way, in PowerShell it is generally better to work with casts (e.g.
      [datetime] '01/25/2025'), as they are culture-invariant (they use [cultureinfo]::InvariantCulture), which means that the result is not dependent on what .NET culture happens to be in effect (note, however, that the invariant culture is based on the US one, so that, as implied by the example above, the first two-digit number is interpreted as the month).

      • By contrast, passing a timestamp string as an argument to a binary cmdlet such as Get-Date is subject to interpretation by the effective culture.

      • This is a known inconsistency that - unfortunately - won't be fixed so as not to break backward compatibility; see GitHub issue #6989.