PowerShell, formatting values in another culture

While Keith Hill’s helpful answer shows you how to change a script’s current culture on demand (more modern alternative as of PSv3+ and .NET framework v4.6+:
[cultureinfo]::CurrentCulture = [cultureinfo]::InvariantCulture), there is no need to change the culture, because – as you’ve discovered in your second update to the question – PowerShell’s string interpolation – as opposed to using the -f operator – always uses the invariant rather than the current culture:

In other words:

If you replace 'val: {0}' -f 1.2 with "val: $(1.2)", the number literal 1.2 is not formatted according to the rules of the current culture.
You can verify in the console by running (on a single line; PSv3+, .NET framework v4.6+):

 PS> [cultureinfo]::currentculture="de-DE"; 'val: {0}' -f 1.2; "val: $(1.2)"
 val: 1,2 # -f operator: GERMAN culture applies, where ',' is the decimal mark
 val: 1.2 # string interpolation: INVARIANT culture applies, where '.' is the decimal mark.

Note: In PowerShell (Core) 7+, the change to a different culture remains in effect for the remainder of the session (as it arguably should for Windows PowerShell too, but doesn’t).


Background:

Surprisingly, PowerShell always applies the invariant rather than the current culture in the following string-related contexts, if the type at hand supports culture-specific conversion to and from strings:

As explained in this in-depth answer, PowerShell explicitly requests culture-invariant processing, if possible – by passing the [cultureinfo]::InvariantCulture instance – in the following scenarios (all this functionality is mediated via .psobject.ToString()):

  • When string-interpolating: if the object’s type implements the IFormattable interface.

  • When casting:

    • to a string, including implicit conversion when binding to a [string]-typed parameter: if the source type implements the [IFormattable] interface.

    • from a string: if the target type’s static .Parse() method has an overload with an [IFormatProvider]-typed parameter (which is an interface implemented by [cultureinfo]).

  • When string-comparing (-eq, -lt, -gt) , using a String.Compare() overload that accepts a CultureInfo parameter.

  • Others?

Note that, separately, custom stringification is applied in casts / implicit stringification for the following .NET types:

  • Arrays and, more generally, similar list-like collection types that PowerShell enumerates in the pipeline (see the bottom section of this answer for what those types are).

    • The (stringified) elements of such types are concatenated with spaces (strictly speaking: with the string specified in the rarely used $OFS preference variable); the stringification of the elements is recursively subject to the rules described here.

    • E.g, [string] (1, 2) yields '1 2'

  • [pscustomobject]

    • Such instances result in a hashtable-like string format described in this answer; e.g.:

      # -> '@{foo=1; bar=2}'
      [string] ([pscustomobject] @{ foo = 1; bar = 2 })
      
    • The fact that calling .ToString() directly on a [pscustomobject] instance does not yield this representation and instead returns the empty string should be considered a bug – see GitHub issue #6163.

  • Others?

As for what the invariant culture is / is for:

The invariant culture is culture-insensitive; it is associated with the English language but not with any country/region.
[…]
Unlike culture-sensitive data, which is subject to change by user customization or by updates to the .NET Framework or the operating system, invariant culture data is stable over time and across installed cultures and cannot be customized by users. This makes the invariant culture particularly useful for operations that require culture-independent results, such as formatting and parsing operations that persist formatted data, or sorting and ordering operations that require that data be displayed in a fixed order regardless of culture.

Presumably, it is the stability across cultures that motivated PowerShell’s designers to consistently use the invariant culture when implicitly converting to and from strings.

For instance, if you hard-code a date string such as '7/21/2017' into a script and later try to convert it to date with a [date] cast, PowerShell’s culture-invariant behavior ensures that the script doesn’t break even when run while a culture other than US-English is in effect – fortunately, the invariant culture also recognizes ISO 8601-format date and time strings;
e.g., [datetime] '2017-07-21' works too.

On the flip side, if you do want to convert to and from current-culture-appropriate strings, you must do so explicitly.

To summarize:

  • Converting to strings:

    • Embedding instances of data types with culture-sensitive-by-default string representations inside "..." yields a culture-invariant representation ([double] or [datetime] are examples of such types).
    • To get a current-culture representation, call .ToString() explicitly or use -f), the formatting operator (possibly inside "..." via an enclosing $(...)).
  • Converting from strings:

    • A direct cast ([<type>] ...) only ever recognizes culture-invariant string representations.

    • To convert from a current-culture-appropriate string representation (or a specific culture’s representation), use the target type’s static ::Parse() method explicitly (optionally with an explicit [cultureinfo] instance to represent a specific culture).


Culture-INVARIANT examples:

  • string interpolation and casts:

    • "$(1/10)" and [string] 1/10

      • both yield string literal 0.1, with decimal mark ., irrespective of the current culture.
    • Similarly, casts from strings are culture-invariant; e.g., [double] '1.2'

      • . is always recognized as the decimal mark, irrespective of the current culture.
      • Another way of putting it: [double] 1.2 is not translated to the culture-sensitive-by-default method overload [double]::Parse('1.2'), but to the culture-invariant [double]::Parse('1.2', [cultureinfo]::InvariantCulture)
  • string comparison (assume that [cultureinfo]::CurrentCulture="tr-TR" is in effect – Turkish, where i is NOT a lowercase representation of I)

    • [string]::Equals('i', 'I', 'CurrentCultureIgnoreCase')
      • $false with the Turkish culture in effect.
      • 'i'.ToUpper() shows that in the Turkish culture the uppercase is İ, not I.
    • 'i' -eq 'I'
      • is still $true, because the invariant culture is applied.
      • implicitly the same as: [string]::Equals('i', 'I', 'InvariantCultureIgnoreCase')

Culture-SENSITIVE examples:

The current culture IS respected in the following cases:

  • With -f, the string-formatting operator (as noted above):

    • [cultureinfo]::currentculture="de-DE"; '{0}' -f 1.2 yields 1,2
    • Pitfall: Due to operator precedence, any expression as the RHS of -f must be enclosed in (...) in order to be recognized as such:
      • E.g., '{0}' -f 1/10 is evaluated as if ('{0}' -f 1) / 10 had been specified;
        use '{0}' -f (1/10) instead.
  • Default output to the console:

    • e.g., [cultureinfo]::CurrentCulture="de-DE"; 1.2 yields 1,2

    • The same applies to output from cmdlets; e.g.,
      [cultureinfo]::CurrentCulture="de-DE"; Get-Date '2017-01-01' yields
      Sonntag, 1. Januar 2017 00:00:00

    • Caveat: In certain scenarios, literals passed to a script block as unconstrained parameters can result in culture-invariant default output – see GitHub issue #4557 and GitHub issue #4558.

  • When writing to a file with Set-Content/Add-Content or Out-File / > / >>:

    • e.g., [cultureinfo]::CurrentCulture="de-DE"; 1.2 > tmp.txt; Get-Content tmp.txt yields 1,2
  • When using the static ::Parse() / ::TryParse() methods on number types such as [double] while passing only the string to parse; e.g., with culture fr-FR in effect (where , is the decimal mark), [double]::Parse('1,2') returns double 1.2 (i.e., 1 + 2/10).

    • Caveat: As bviktor points out, thousands separators are recognized by default, but in a very loose fashion: effectively, the thousands separator can be placed anywhere inside the integer portion, irrespective of how many digits are in the resulting groups, and a leading 0 is also accepted; e.g., in the en-US culture (where , is the thousands separator), [double]::Parse('0,18') perhaps surprisingly succeeds and yields 18.
      • To suppress recognition of thousands separators, use something like [double]::Parse('0,18', 'Float'), via the NumberStyles parameter
  • Unintentional culture-sensitivity that won’t be corrected to preserve backward compatibility:

  • Others?

Leave a Comment