Search code examples
powershellunix-timestamp

How to convert a long to a string without having the scientific notation


I have a Powershell script that get the current time and convert it in Unix Timestamp. I would like to use this value as string parameter to make an HTTP request:

$currentTime = [math]::Round([double](get-date -uformat %s ((get-date).AddMinutes(-5).ToUniversalTime()))*1000)

Invoke-RestMethod -Method Delete -Uri "http://something?unmodifiedSince=$currentTime"

On some Windows machine it works fine but on some other (different Region settings?), I get the current time converted with the scientific notation. For example

http://something?unmodifiedSince=1.53835531189786E+17

How to avoid this conversion?


Solution

  • Update:

    [datetimeoffset]::UtcNow.AddMinutes(-5).ToUnixTimeSeconds()
    

    tl;dr:

    To work around the problem with culture-specific number formatting, avoid a [double] cast and use [double]::Parse() instead, which by default parses in a culture-sensitive manner:

    [Math]::Round(
      [double]::Parse(
         (Get-Date -Uformat %s ((Get-Date).ToUniversalTime().AddMinutes(-5)))
      ) * 1000,
      0,
      'AwayFromZero'
    )
    

    The AwayFromZero midpoint-rounding strategy ensures that .5 second values are always rounded up, whereas the default ToEven strategy would situationally round down, namely whenever the integer part of the number happens to be even.


    Indeed, your problem stems from the fact that:

    • Get-Date -UFormat % outputs a string representation of the Unix timestamp
    • and uses culture-sensitive formatting for the underlying floating-point number[1], which means that in some cultures you'll get a string such as '1538651788,87456' (, as the decimal mark) rather than '1538651788.87456' as the output.

    By contrast, PowerShell's casts always use the invariant culture, which only recognizes . as the decimal mark - and ignores ,, which is considered a thousands-grouping character.

    PS> [double] '1538651788,87456'
    153865178887456  # !! , was IGNORED
    

    Because the decimal mark was ignored and there are 5 decimal places, the resulting number is too large by a factor of 10,000 in this case (though note that the number of decimal places can vary, because trailing zeros aren't displayed).

    If you then multiply that result with 1000, you get a number so large that PowerShell defaults its string representation to the scientific format that you've experienced:

    PS> [double] '1538651788,87456' * 1000
    1.53865178887456E+17 # !! scientific notation.
    

    [1] Optional reading: Get-Date -UFormat %s problems in Windows PowerShell vs. PowerShell Core (v6+):

    • Unix timestamps are integers, so Get-Date -UFormat %s shouldn't return a floating-point number to begin with. This problem has been corrected in PowerShell Core.

    • Unix timestamps are expressed in UTC, but Windows PowerShell only returns the correct value if you explicitly pass a UTC [datetime] instance. This problem has been corrected in PowerShell Core (v6+).

      • E.g., to get the current time's Unix timestamp in Windows PowerShell, using
        Get-Date -UFormat %s is not enough; use Get-Date -UFormat %s ([datetime]::UtcNow) instead.

    In short: This question's problem wouldn't have arisen in PowerShell Core, because string representations of integers aren't culture-sensitive; additionally, the need for rounding goes away, as does the need to convert the input date to UTC, so that a PowerShell Core solution simplifies to:

    # PowerShell *Core* only 
    1000 * (Get-Date -UFormat %s ((Get-Date).AddMinutes(-5)))
    

    Note: This technically returns a [double] instance, but one without decimal places; use [long] (...) if an integer type is expressly needed.