I have a inventory database from my company and I'm wanting to sort some entries based on pricing. I was thinking originally I would have to do everything by hand but I figured Sort-Object should work... until I remembered Sort-Object and its infamous string sorting. Easy, i'll sort by converting it to an integer except of course a currency value has symbol such as $ at the start.
The original code I used which caused the string sorting is below. The classic 200 is higher than 1000 etc:
$Result | Sort-Object -Property Price | Format-Table -Property Price
The int code I tried is:
$Result | Sort-Object -Property { [int]$_.Price } | Format-Table -Property Price
This results in output like "Cannot convert value "$414.50" to type "System.Int32". | Error: "Input string was not in a correct format." Makes sense, cant convert a $ to an int.
So is there any way around this without me having to sort by hand?
Thanks
To add to mclayton's helpful answer:
It is simpler to use a predefined [cultureinfo] instance that uses the your currency format, such as en-US (US-English) in the [decimal]::Parse() call, in combination with C, the currency format specifier.
#(
[pscustomobject] #{ Price='$414.50' },
[pscustomobject] #{ Price='99.02$' }
[pscustomobject] #{ Price='999.03' }
[pscustomobject] #{ Price='$5.04' }
) |
Sort-Object { [decimal]::Parse($_.Price, 'C', [cultureinfo] 'en-US') }
Output (correctly numerically sorted):
Price
-----
$5.04
99.02$
$414.50
999.03
Note:
As the sample input values show, there's some flexibility with respect to what input formats are accepted, such as a trailing $, and a value without $_.
If the current culture can be assumed to be en-US (or a different culture that uses the same currency symbol and formatting, notably also the same decimal separator, .), you can omit the [cultureinfo] 'en-US' argument in the [decimal]::Parse() call above - though for robustness I suggest keeping it.
As an aside: PowerShell's casts (which don't support currency values) always use the invariant culture with string operands, irrespective of the current culture. Thus, something like [decimal] '3.14' is recognized even while a culture that uses , as the decimal separator is in effect.
While the invariant culture - whose purpose is to provide representations that aren't culture-dependent and remain stable over time - is based on the US-English culture, it can not be used here, because its currency symbol is ¤; e.g., (9.99).ToString('C', [cultureinfo]::InvariantCulture) yields ¤9.99.
An input value that cannot be parsed as a currency causes an (effectively) non-terminating error,[1] and such values sort before the currency values.
If you simply want to ignore non-conforming values, use try { [decimal]::Parse(...) } catch { }
If you want to abort processing on encountering non-confirming values pass -ErrorAction Stop to the Sort-Object call.
[1] A .NET method call that fails causes a statement-terminating error, but since the error occurs in a script block (in the context of a calculated property), only the statement inside the script block is terminated, not the enclosing Sort-Object call
Firstly, you probably want [decimal] instead of [int] because [int] "414.50" is 414, not 414.50 so you'll be losing precision.
That aside, I'm adapting this answer for C#: https://stackoverflow.com/a/56603818/3156906
$fi = new-object System.Globalization.NumberFormatInfo;
$fi.CurrencySymbol = "`$";
#("`$10.00", "`$2.00") | Sort-Object -Property #{
"Expression" = { [decimal]::Parse($_, "Currency", $fi) }
};
# $2.00
# $10.00
The advantage of this is that invalid database values like - e.g. $1.$10 - that might have crept in will throw an exception, as will different currencies like £1.00 so you're getting a bit of extra data validation for free.
Note that the results remain as strings, but they're sorted as currency amounts (decimals). If you want the actual numeric value you'll need to convert the values separately...
Related
I've tried really hard not ask this question, but I keep coming back to it as I'm not sure if I'm doing everything as efficiently as I can or if there might be problems under the hood. Basically, I have a CSV file that contains a number field, but it includes a decimal and values out to the ten-thousandths place, e.g. 15.0000. All I need to do is convert that to a whole number without the decimal place.
I came across a related question here, but the selected answer seems to cast doubt on casting the string representation directly to an integer data type - without explaining why.
Simply casting the string as an int won't work reliably. You need to convert it to an int32.
I've haven't had much luck getting the [System.Convert] method to work, or doing something like $StringNumber.ToInt32(). I realize that once I save the data back to the PSCustomObject they'll be stored as strings, so at the end of the day maybe I'm making this even more complicated than necessary for my use case and I just need to reformat $StringNumber...but even that has caused me some problems.
Any ideas on why casting wouldn't be reliable or better ways to handle this in my case?
Examples of what I've tried:
PS > $StringNumber = '15.0000'
PS > [Convert]::ToInt32($StringNumber)
#MethodInvocationException: Exception calling "ToInt32" with "1" argument(s): "Input string was not in a correct format."
PS > [Convert]::ToInt32($StringNumber, [CultureInfo]::InvariantCulture)
#MethodInvocationException: Exception calling "ToInt32" with "2" argument(s): "Input string was not in a correct format."
PS > $StringNumber.ToInt32()
#MethodException: Cannot find an overload for "ToInt32" and the argument count: "0".
PS > $StringNumber.ToInt32([CultureInfo]::InvariantCulture)
#MethodInvocationException: Exception calling "ToInt32" with "1" argument(s): "Input string was not in a correct format."
PS > $StringNumber.ToString("F0")
#MethodException: Cannot find an overload for "ToString" and the argument count: "1".
PS > $StringNumber.ToString("F0", [CultureInfo]::CurrentCulture)
#MethodException: Cannot find an overload for "ToString" and the argument count: "2".
PS > "New format: {0:F0}" -f $StringNumber
#New format: 15.0000
So basically what I've come up with is:
Someone in 2014 said casting my string to an int wouldn't work reliably, even though it seems like the Cast operator is actually doing a conversion
The ToInt32 methods don't like strings with decimals as the input
Apparently String.ToString Method is useless
Thanks to String.ToString and the processing order of composite formatting, simple "reformatting" of my string representation won't work
In summary: Is there a way to safely cast my $StringNumber into a whole number, and, if so, what's the most efficient way to do it on a large dataset?
Bonus Challenge:
If anyone can make this work using the ForEach magic method then I'll buy you a beer. Here's some pseudo code that doesn't work, but would be awesome if it did. As far as I can figure out, there's no way to reference the current item in the collection when setting the value of a string property
#This code DOES NOT work as written
PS > $CSVData = Import-Csv .\somedata.csv
PS > $CSVData.ForEach('StringNumberField', [int]$_.StringNumberField)
If your string representation can be interpreted as a number, you can cast it to an integer, as long as the specific integer type used is large enough to accommodate (the integer portion of) the value represented (e.g. [int] '15.0000')
A string that can not be interpreted as a number or represents a number that is too large (or small, for negative numbers) for the target type, results in a statement-terminating error; e.g. [int] 'foo' or [int] '444444444444444'
Note that PowerShell's casts and implicit string-to-number conversions use the invariant culture, which means that only ever . is recognized as the decimal mark (and , is effectively ignored, because it is interpreted as the thousands-grouping symbol), irrespective of the culture currently in effect (as reflected in $PSCulture).
As for integer types you can use (all of them - except the open-ended [bigint] type - support ::MinValue and ::MaxValue to determine the range of integers they can accommodate; e.g. [int]::MaxValue)
Signed integer types: [sbyte], [int16], [int] ([int32]), [long] ([int64]), [bigint]
Unsigned integer types: [byte], [uint16], [uint] ([uint32]), [ulong] ([uint64]) - but note that PowerShell itself uses only signed types natively in its calculations.
Casting to an integer type performs half-to-even midpoint rounding, which means that a string representing a value whose fractional part is .5 is rounded to the nearest even integer; e.g. [int] '1.5' and [int] '2.5' both round to 2.
To choose a different midpoint rounding strategy, use [Math]::Round() with a System.MidpointRounding argument; e.g.:
[Math]::Round('2.5', [MidPointRounding]::AwayFromZero) # -> 3
To unconditionally round up or down to the nearest integer, use [Math]::Ceiling(), [Math]::Floor(), or [Math]::Truncate(); e.g.:
[Math]::Ceiling('2.5') # -> 3
[Math]::Floor('2.5') # -> 2
[Math]::Truncate('2.5') # -> 2
#
[Math]::Ceiling('-2.5') # -> -2
[Math]::Floor('-2.5') # -> -3
[Math]::Truncate('-2.5') # -> -2
Note: While the resulting number is conceptually an integer, technically it is a [double] or - with explicit [decimal] or integer-number-literal input - a [decimal].
As for the bonus challenge:
With an integer-type cast:
[int[]] (Import-Csv .\somedata.csv).StringNumberField
Note: (Import-Csv .\somedata.csv).StringNumberField.ForEach([int]) would work too, but offers no advantage here.
With a [Math]::*() call and the .ForEach() array method:
(Import-Csv .\somedata.csv).StringNumberField.ForEach(
{ [Math]::Round($_, [MidPointRounding]::AwayFromZero) }
)
Casting [int] as you explained, is something that would work in most cases, however it is also prone to errors. What if the number is higher than [int]::MaxValue ? The alternative you could use to avoid the exceptions would be to use the -as [int] operator however there is another problem with this, if the value cannot be converted to integer you would be getting $null as a result.
To be safe that the string will be converted and you wouldn't get null as a result first you need to be 100% sure that the data you're feeding is correct or assume the worst and use [math]::Round(..) in combination with -as [decimal] or -as [long] or -as [double] (∞) to round your numbers:
[math]::Round('123.123' -as [decimal]) # => 123
[math]::Round('123.asd' -as [decimal]) # => 0
Note: I'm using round but [math]::Ceiling(..) or [math]::Floor(..) or [math]::Truncate(..) are valid alternatives too, depending on your expected output.
Another alternative is to use [decimal]::TryParse(..) however this would throw if there ever be something that is not a number:
$StringNumber = '15.0000'
$ref = 0
[decimal]::TryParse( $StringNumber, ([ref]$ref) )
[math]::Round($ref) # => 15
Using Hazrelle's advise would work too but again, would throw an exception for invalid input or "Value was either too large or too small for an Int32."
[System.Decimal]::ToInt32('123123123.123') # => 123123123
As for the Bonus Challenge, I don't think it's possible to cast and then set the rounded values to your CSV on just one go using ForEach(type convertToType), and even if it was, it could also bring problems because of what was mentioned before:
$csv = #'
"Col1","Col2"
"val1","15.0000"
"val2","20.123"
"val3","922337203685477.5807"
'# | ConvertFrom-Csv
$csv.Col2.ForEach([int])
Cannot convert argument "item", with value: "922337203685477.5807", for "Add" to type "System.Int32": "Cannot convert value "922337203685477.5807" to type "System.Int32".
Using .foreach(..) array method combined with a script block would work:
$csv.ForEach({
$_.Col2 = [math]::Round($_.Col2 -as [decimal])
})
In case you wonder why not just use [math]::Round(..) over the string and forget about it:
[math]::Round('123.123') # => 123 Works!
But what about:
PS /> [math]::Round([decimal]::MaxValue -as [string])
7.92281625142643E+28
PS /> [math]::Round([decimal]([decimal]::MaxValue -as [string]))
79228162514264337593543950335
Is there an easy way in PowerShell to format numbers and the like in another locale? I'm currently writing a few functions to ease SVG generation for me and SVG uses . as a decimal separator, while PowerShell honors my locale settings (de-DE) when converting floating-point numbers to strings.
Is there an easy way to set another locale for a function or so without sticking
.ToString((New-Object Globalization.CultureInfo ""))
after every double variable?
Note: This is about the locale used for formatting, not the format string.
(Side question: Should I use the invariant culture in that case or rather en-US?)
ETA: Well, what I'm trying here is something like the following:
function New-SvgWave([int]$HalfWaves, [double]$Amplitude, [switch]$Upwards) {
"<path d='M0,0q0.5,{0} 1,0{1}v1q-0.5,{2} -1,0{3}z'/>" -f (
$(if ($Upwards) {-$Amplitude} else {$Amplitude}),
("t1,0" * ($HalfWaves - 1)),
$(if ($Upwards -xor ($HalfWaves % 2 -eq 0)) {-$Amplitude} else {$Amplitude}),
("t-1,0" * ($HalfWaves - 1))
)
}
Just a little automation for stuff I tend to write all the time and the double values need to use the decimal point instead of a comma (which they use in my locale).
ETA2: Interesting trivia to add:
PS Home:> $d=1.23
PS Home:> $d
1,23
PS Home:> "$d"
1.23
By putting the variable into a string the set locale doesn't seem to apply, somehow.
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:
By design,[1] but perhaps surprisingly, PowerShell 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 (the stringification PowerShell performs is the equivalent of calling .psobject.ToString([NullString]::Value, [cultureinfo]::InvariantCulture) on a value):
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.2}'; values are formatted with the *invariant* culture
[string] ([pscustomobject] #{ foo = 1; bar = 2.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 the purpose of the invariant culture:
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.
In (all?) cmdlets:
Those that that perform equality comparisons:
Select-Object with the -Unique switch; also note that - unusually - case-sensitive comparison is performed, and as of PowerShell 7.2.4 case-insensitivity isn't even available as an opt-in - see GitHub issue #12059.
Select-Object
Compare-Object
Others?
Those that write to files:
Set-Content and Add-Content
Out-File and therefore its virtual alias, > (and >>)
e.g., [cultureinfo]::CurrentCulture = 'de-DE'; 1.2 > tmp.txt; Get-Content tmp.txt yields 1,2
Due to .NET's logic, 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:
In parameter-binding type conversions for compiled cmdlets (but PowerShell code - scripts or functions - is culture-invariant) - see GitHub issue #6989.
In the -as operator - see GitHub issue #8129.
In [hashtable] key lookups - see this answer and GitHub issue #8280.
[Fixed in v7.1+] In the LHS of -replace operations - see GitHub issue #10948.
Others?
[1] The aim is to support programmatic processing using representations that do not vary by culture and do not change over time. See the linked quote from the docs later in the answer.
This is a PowerShell function I use for testing script in other cultures. I believe it could be used for what you are after:
function Using-Culture ([System.Globalization.CultureInfo]$culture =(throw "USAGE: Using-Culture -Culture culture -Script {scriptblock}"),
[ScriptBlock]$script=(throw "USAGE: Using-Culture -Culture culture -Script {scriptblock}"))
{
$OldCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture
$OldUICulture = [System.Threading.Thread]::CurrentThread.CurrentUICulture
try {
[System.Threading.Thread]::CurrentThread.CurrentCulture = $culture
[System.Threading.Thread]::CurrentThread.CurrentUICulture = $culture
Invoke-Command $script
}
finally {
[System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture
[System.Threading.Thread]::CurrentThread.CurrentUICulture = $OldUICulture
}
}
PS> $res = Using-Culture fr-FR { 1.1 }
PS> $res
1.1
I was thinking about how to make it easy and came up with accelerators:
Add-type -typedef #"
using System;
public class InvFloat
{
double _f = 0;
private InvFloat (double f) {
_f = f;
}
private InvFloat(string f) {
_f = Double.Parse(f, System.Globalization.CultureInfo.InvariantCulture);
}
public static implicit operator InvFloat (double f) {
return new InvFloat(f);
}
public static implicit operator double(InvFloat f) {
return f._f;
}
public static explicit operator InvFloat (string f) {
return new InvFloat (f);
}
public override string ToString() {
return _f.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
}
"#
$acce = [type]::gettype("System.Management.Automation.TypeAccelerators")
$acce::Add('f', [InvFloat])
$y = 1.5.ToString()
$z = ([f]1.5).ToString()
I hope it will help.
If you already have the culture loaded in your environment,
#>Get-Culture
LCID Name DisplayName
---- ---- -----------
1031 de-DE German (Germany)
#>Get-UICulture
LCID Name DisplayName
---- ---- -----------
1033 en-US English (United States)
it is possible to resolve this problem:
PS Home:> $d=1.23
PS Home:> $d
1,23
like this:
$d.ToString([cultureinfo]::CurrentUICulture)
1.23
Of course you need to keep in mind that if other users run the script with a different locale setting, the results may not turn out as originally intended.
Nevertheless, this solution could come in useful. Have fun!
I'm fairly new to powershell scripting after years of unix/linux scripting and I'm still trying to get the handle of object output. I am trying to run the Get-Alias command to return just the column "Name." When I try to select just the column "name" I either get the whole get-alias output, or errors.
The script/ command line I am trying to use is: get-alias | format-table
When I add select-object logic, it either cuts off the name of the alias and leaves off the command name or the select-object logic is just ignored. I'm sure the solution is simple. Thanks
tl;dr:
PS> Get-Alias | Select-Object -ExpandProperty DisplayName
? -> Where-Object
cd -> Set-Location
...
That is, the true property name underlying the Name display column is DisplayName.
PowerShell's default output formatting sometimes uses column names that differ from or the underlying objects' property names (which is the case here) and/or use values that are calculated.
What shows as column Name by default is actually the underlying objects' .DisplayName property, whereas the true .Name property contains just the alias' own name.
You can inspect the members (properties and methods) of an object's type via Get-Member or, to see both the property (but not method) names and their concrete values for a given object, pipe to Format-List *:
PS> Get-Alias | Select-Object -First 1 | Format-List *
HelpUri : https://go.microsoft.com/fwlink/?LinkID=113423
ResolvedCommandName : Where-Object
DisplayName : ? -> Where-Object
ReferencedCommand : Where-Object
ResolvedCommand : Where-Object
Definition : Where-Object
Options : ReadOnly, AllScope
Description :
OutputType : {}
Name : ?
CommandType : Alias
Source :
Version :
Visibility : Public
ModuleName :
Module :
RemotingCapability : None
Parameters : {[InputObject, System.Management.Automation.ParameterMetadata], [FilterScript, System.Management.Automation.ParameterMetadata], [Property,
System.Management.Automation.ParameterMetadata], [Value, System.Management.Automation.ParameterMetadata]...}
ParameterSets :
Optional reading: How to inspect a given type's formatting data:
For background information, see Get-Help about_Format.ps1xml.
Get-Alias outputs [System.Management.Automation.AliasInfo] instances (as Get-Member will tell you).
Using the Get-FormatData cmdlet you can inspect a type's formatting data:
Caveat: Not all formatting data in effect is by default reported by Get-FormatData for backward-compatibility reasons - see this GitHub issue; notable omissions are [System.IO.FileInfo] and [System.IO.DirectoryInfo], as returned by Get-ChildItem / Get-Item ; in short, to see all formatting data in effect for the PowerShell version at hand, use:
Get-FormatData -PowerShellVersion $PSVersionTable.PSVersion [...]
Somewhat ironically, there is no custom formatting data defined for the System.Management.Automation.ExtendedTypeDefinition instances that Get-Format outputs, and the default formatting isn't helpful.
Currently, the simplest way to inspect formatting data is to pipe to the Format-Custom cmdlet, which provides an informal, human-readable representation of the internal structure of objects:
# Best to send this to a file and open that file in a text editor for perusal.
$prev, $FormatEnumerationLimit = $FormatEnumerationLimit, [int]::MaxValue
Get-FormatData System.Management.Automation.AliasInfo | Format-Custom -Depth 10
$FormatEnumerationLimit = $prev
Note:
$FormatEnumerationLimit is temporarily set to the highest possible value to ensure that all column / property definitions are shown in the output (by default, only up to 4 would be shown, and omitted ones would be indicated with ... (Windows PowerShell, 3 chars.) / … (PowerShell Core, 1 char.).
-Depth 10 ensures that the internal structure of the output objects is fully represented; by default, only up to 5 levels would be.
The output will look vaguely like JSON, and is suited to visual inspection only, not programmatic processing.
You'll see one or more of the following blocks:
class ExtendedTypeDefinition
{
TypeNames =
[
<typeName>
]
FormatViewDefinition =
[
class FormatViewDefinition
{
Name = <formatName>
Control =
class <viewType>Control
...
}
class FormatViewDefinition
{
Name = <formatName>
Control =
class <viewType>Control
...
}
}
<typeName> is the full type name to which the format applies (e.g., System.Management.Automation.AliasInfo)
<formatName> is an internally used, non-standardized name for the format-definition at hand
<viewType>Control is one of the following classes, defining the views for use with the corresponding Format-Table,Format-List, Format-Wide, and Format-Custom cmdlets: TableControl, ListControl, WideControl, CustomControl, all derived from the System.Management.Automation.PSControl base class.
Note that the output may contain duplicate information, as it does in the case of System.Management.Automation.AliasInfo (because the formatting information is repeated for all other command types that share the same formatting), but it is sufficient to examine the first class <viewType>Control instance of interest.
Specifically, here's an excerpt from the first class TableControl block:
class TableControl # Defines the format for Format-Table
{
Headers = # The table headers (column names and column properties)
...
class TableControlColumnHeader # The "Name" column *header*
{
Label = Name
Alignment = Undefined
Width = 50
}
...
]
Rows = # The table rows (underlying or calculated property values)
[
class TableControlRow
{
Columns =
[
...
class TableControlColumn # The "Name" column *value*
{
Alignment = Undefined
DisplayEntry =
class DisplayEntry
{
ValueType = ScriptBlock # A *calculated* property
Value =
if ($_.CommandType -eq "Alias")
{
$_.DisplayName
}
else
{
$_.Name
}
}
FormatString =
}
...
]
...
Note that the header and column-value definitions are matched positionally; e.g., the 2nd TableControlColumn instance defines the value for the 2nd TableControlColumnHeader instance.
The above explains why, even though the column is entitled "Name", it is an alias' .DisplayName property that is displayed, whereas all the other command types are represented by their .Name property, by way of the embedded script block (the piece of PowerShell code in which $_ represents the input object at hand).
Presumably, the decision to show the .DisplayName for aliases was made to provide more helpful information by default, but the discrepancy between the column name and underlying property name can certainly lead to confusion.
Note that there is also a dedicated table view for type [System.Management.Automation.AliasInfo] that (a) defines only 2 columns, and (b) calls the .DisplayProperty column values by their true name.
However, as of the PowerShell versions listed above, this view is preempted by the multi-type definition discussed above and effectively ignored.
can you please tell me how to remove currency formatting from a variable (which is probably treated as a string).
How do I strip out currency formatting from a variable and convert it to a true number?
Thank you.
example
PS C:\Users\abc> $a=($464.00)
PS C:\Users\abc> "{0:N2}" -f $a
<- returns blank
However
PS C:\Users\abc> $a=-464
PS C:\Users\abc> "{0:C2}" -f $a
($464.00) <- this works
PowerShell, the programming language, does not "know" what money or currency is - everything PowerShell sees is a variable name ($464) and a property reference (.00) that doesn't exist, so $a ends up with no value.
If you have a string in the form: $00.00, what you can do programmatically is:
# Here is my currency amount
$mySalary = '$500.45'
# Remove anything that's not either a dot (`.`), a digit, or parentheses:
$mySalary = $mySalary -replace '[^\d\.\(\)]'
# Check if input string has parentheses around it
if($mySalary -match '^\(.*\)$')
{
# remove the parentheses and add a `-` instead
$mySalary = '-' + $mySalary.Trim('()')
}
So far so good, now we have the string 500.45 (or -500.45 if input was ($500.45)).
Now, there's a couple of things you can do to convert a string to a numerical type.
You could explicitly convert it to a [double] with the Parse() method:
$mySalaryNumber = [double]::Parse($mySalary)
Or you could rely on PowerShell performing an implicit conversion to an appropriate numerical type with a unary +:
$mySalaryNumber = +$mySalary
Is there an easy way in PowerShell to format numbers and the like in another locale? I'm currently writing a few functions to ease SVG generation for me and SVG uses . as a decimal separator, while PowerShell honors my locale settings (de-DE) when converting floating-point numbers to strings.
Is there an easy way to set another locale for a function or so without sticking
.ToString((New-Object Globalization.CultureInfo ""))
after every double variable?
Note: This is about the locale used for formatting, not the format string.
(Side question: Should I use the invariant culture in that case or rather en-US?)
ETA: Well, what I'm trying here is something like the following:
function New-SvgWave([int]$HalfWaves, [double]$Amplitude, [switch]$Upwards) {
"<path d='M0,0q0.5,{0} 1,0{1}v1q-0.5,{2} -1,0{3}z'/>" -f (
$(if ($Upwards) {-$Amplitude} else {$Amplitude}),
("t1,0" * ($HalfWaves - 1)),
$(if ($Upwards -xor ($HalfWaves % 2 -eq 0)) {-$Amplitude} else {$Amplitude}),
("t-1,0" * ($HalfWaves - 1))
)
}
Just a little automation for stuff I tend to write all the time and the double values need to use the decimal point instead of a comma (which they use in my locale).
ETA2: Interesting trivia to add:
PS Home:> $d=1.23
PS Home:> $d
1,23
PS Home:> "$d"
1.23
By putting the variable into a string the set locale doesn't seem to apply, somehow.
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:
By design,[1] but perhaps surprisingly, PowerShell 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 (the stringification PowerShell performs is the equivalent of calling .psobject.ToString([NullString]::Value, [cultureinfo]::InvariantCulture) on a value):
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.2}'; values are formatted with the *invariant* culture
[string] ([pscustomobject] #{ foo = 1; bar = 2.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 the purpose of the invariant culture:
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.
In (all?) cmdlets:
Those that that perform equality comparisons:
Select-Object with the -Unique switch; also note that - unusually - case-sensitive comparison is performed, and as of PowerShell 7.2.4 case-insensitivity isn't even available as an opt-in - see GitHub issue #12059.
Select-Object
Compare-Object
Others?
Those that write to files:
Set-Content and Add-Content
Out-File and therefore its virtual alias, > (and >>)
e.g., [cultureinfo]::CurrentCulture = 'de-DE'; 1.2 > tmp.txt; Get-Content tmp.txt yields 1,2
Due to .NET's logic, 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:
In parameter-binding type conversions for compiled cmdlets (but PowerShell code - scripts or functions - is culture-invariant) - see GitHub issue #6989.
In the -as operator - see GitHub issue #8129.
In [hashtable] key lookups - see this answer and GitHub issue #8280.
[Fixed in v7.1+] In the LHS of -replace operations - see GitHub issue #10948.
Others?
[1] The aim is to support programmatic processing using representations that do not vary by culture and do not change over time. See the linked quote from the docs later in the answer.
This is a PowerShell function I use for testing script in other cultures. I believe it could be used for what you are after:
function Using-Culture ([System.Globalization.CultureInfo]$culture =(throw "USAGE: Using-Culture -Culture culture -Script {scriptblock}"),
[ScriptBlock]$script=(throw "USAGE: Using-Culture -Culture culture -Script {scriptblock}"))
{
$OldCulture = [System.Threading.Thread]::CurrentThread.CurrentCulture
$OldUICulture = [System.Threading.Thread]::CurrentThread.CurrentUICulture
try {
[System.Threading.Thread]::CurrentThread.CurrentCulture = $culture
[System.Threading.Thread]::CurrentThread.CurrentUICulture = $culture
Invoke-Command $script
}
finally {
[System.Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture
[System.Threading.Thread]::CurrentThread.CurrentUICulture = $OldUICulture
}
}
PS> $res = Using-Culture fr-FR { 1.1 }
PS> $res
1.1
I was thinking about how to make it easy and came up with accelerators:
Add-type -typedef #"
using System;
public class InvFloat
{
double _f = 0;
private InvFloat (double f) {
_f = f;
}
private InvFloat(string f) {
_f = Double.Parse(f, System.Globalization.CultureInfo.InvariantCulture);
}
public static implicit operator InvFloat (double f) {
return new InvFloat(f);
}
public static implicit operator double(InvFloat f) {
return f._f;
}
public static explicit operator InvFloat (string f) {
return new InvFloat (f);
}
public override string ToString() {
return _f.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
}
"#
$acce = [type]::gettype("System.Management.Automation.TypeAccelerators")
$acce::Add('f', [InvFloat])
$y = 1.5.ToString()
$z = ([f]1.5).ToString()
I hope it will help.
If you already have the culture loaded in your environment,
#>Get-Culture
LCID Name DisplayName
---- ---- -----------
1031 de-DE German (Germany)
#>Get-UICulture
LCID Name DisplayName
---- ---- -----------
1033 en-US English (United States)
it is possible to resolve this problem:
PS Home:> $d=1.23
PS Home:> $d
1,23
like this:
$d.ToString([cultureinfo]::CurrentUICulture)
1.23
Of course you need to keep in mind that if other users run the script with a different locale setting, the results may not turn out as originally intended.
Nevertheless, this solution could come in useful. Have fun!