I am using powershell for some time now, and just stumbled upon the PSKoan Project:
https://github.com/vexx32/PSKoans
On one of the very first koans I found some strange behaviour, I cant explain to my self.
The koan about number types try to teach how powershell will convert variable types dynamically from int to long and from int to double on certain operations.
So I filled out the blanks (as expected during the course) of this specific pester test:
It 'can be a larger number if needed' {
<#
Integers come in two flavours:
- int (Int32)
- long (Int64)
If an integer value exceeds the limits of the Int32 type, it is
automatically expanded to the larger Int64 type.
#>
# What exactly are the limitations of the [int] type?
$MaxValue = [int]::MaxValue
$MinValue = [int]::MinValue
2147483647 | Should -Be $MaxValue
-2147483648 | Should -Be $MinValue
# If you enter a number larger than that, the type should change.
$BigValue = $MaxValue +1
$BigValue | Should -BeOfType [long]
$BigValue | Should -BeGreaterThan $MaxValue
$SmallValue = $MinValue -1
$SmallValue | Should -BeOfType [long]
$SmallValue | Should -BeLessThan $MinValue
}
Use show-karma to let PSKoans to their magic:
The answers you seek...
Expected the value to have type [long] or any of its subtypes, but got 2147483648 with type [double].
This can be narrowed down to the following example, which I dont understand:
Why the heck is this a double now, and not a long?
I am using PS 5.1:
P.S. Its late here, so perhaps I miss something really obvious, I am already prepared for the great face palm ;)
Indeed, in the context of expressions (calculations), PowerShell indeed automatically widens anything that exceeds the max. value of [int] / [uint] (32-bit signed / unsigned integers) or [long] / [ulong] (64-bit) to [double], which is easy to verify[1]:
# Ditto for [uint], [long], [ulong]
PS> ([int]::MaxValue + 1).GetType().Name
Double # [double]
By contrast, integer types smaller than [int] are promoted to [int] (System.Int32) if their max. value is exceeded in a calculation:
# Ditto for [sbyte], [int16], [uint16]
PS> ([byte]::MaxValue + 1).GetType().Name
Int32 # same as: [int]
It is only with number literals that promotion to the next biggest - signed - integer type occurs for values beyond [int]:
Note: Any number literal whose value is between [int]::MinValue and [int]::MaxValue - even if it could fit into a smaller integer type - defaults to [int], i.e., a 32-bit signed integer (System.Int32).
# Note: 2147483648 is the result of ([int]::MaxValue + 1)
PS> (2147483648).GetType().Name
Int64 # same as: [long]
And if a number literal exceeds the value of [long]::MaxValue, promotion to [decimal] occurs:
# Note: 9223372036854775808 is the result of ([long]::MaxValue + 1)
PS> (9223372036854775808).GetType().Name
Decimal # [decimal]
It is only if you exceed [decimal]::MaxValue] that promotion to [double] - with its potential loss of precision - occurs:
# Note: 79228162514264337593543950336 is the result of ([decimal]::MaxValue + 1)
PS> (79228162514264337593543950336).GetType().Name
Double # [double]
Outputting the value above directly makes the conversion to [double] immediately obvious, because the output formatting uses exponential notation: 7.92281625142643E+28
[1] Curiously, trying to calculate a value beyond [decimal]::MaxValue fails, causing a statement-terminating error: ([decimal]::MaxValue + 1).GetType().Name
Related
Ciao all -
I'm using Powershell 7.2 to automate some hardware configuration through the hardware's CLI.
I am using a loop to generate strings that include "0x" prefixes to express hex bytes, but having an issue where any consecutive iterations after the first pass of the loop do not print the "0x" prefix.
The following will produce the issue:
function fTest($id)
{
foreach($n in #(1, 2, 3))
{
write-host $id.gettype()
write-host ("{0:x}" -f $id)
$id++
}
}
fTest 0x1a
Actual output:
System.Int32
0x1a
System.Int32
1b
System.Int32
1c
The 0xprefixes are omitted in iters 2 and 3.
Why is this happening?
What is a clean way to correct the issue?
I'm a PowerShell noob, so I am happy to receive suggestions or examples of entirely different approaches.
Thanks in advance for the help!
tl;dr
Type-constrain your $p parameter to unambiguously make it a number (integer), as Theo suggests:
function fTest($id) -> function fTest([int] $id)
Build the 0x prefix into the format string passed to -f:
"{0:x}" -f $id -> '0x{0:x}' -f $id
Building on the helpful comments:
Why is this happening?
Format string {0:x}, when applied to a number, only ever produces a hexadecimal representation without a 0x prefix; e.g.:
PS> '{0:x}' -f 10
a # NOT '0xa'
If the operand is not a number, the numeric :x specification is ignored:
PS> '{0:x}' -f 'foo'
foo
The problem in your case is related to how PowerShell handles arguments passed to parameters that are not type-constrained:
Argument 0x1a is ambiguous: it could be a number - expressed as hexadecimal constant 0x1a, equivalent to decimal 26 - or a string.
While in expression-parsing mode this ambiguity would not arise (strings must be quoted there), it does in argument-parsing mode, where quoting around strings is optional (except if the string contains metacharacters) - see the conceptual about_Parsing topic.
What PowerShell does in this case is to create a hybrid argument value: The value is parsed as a number, but it caches its original string representation behind the scenes, which is used for display formatting, for instance:
PS> & { param($p) $p; $p.ToString() } 0x1a
0x1a # With default output formatting, the original string form is used.
26 # $p is an [int], so .ToString() yields its decimal representation
As of PowerShell 7.2.2, surprisingly and problematically, in the context of -f, the string-formatting operator, such a hybrid value is treated as a string, even though it self-reports as a number:
PS> & { param($p) $p.GetType().FullName; '{0:N2}' -f $p } 0x1a
System.Int32 # $p is of type [int] == System.Int32
0x1a # !! With -f $p is unexpectedly treated *as a string*,
# !! yielding the cached original string representation.
This unexpected behavior has been reported in GitHub issue #17199.
Type-constraining the parameter to which such a hybrid argument is passed, as shown at the top, avoids the ambiguity: on invocation, the argument is converted to an unwrapped instance of the parameter's type (see next point).
As for why the output changed starting with the 2nd iteration:
The cached string representation is implemented by way of an invisible [psobject] wrapper around the instance of the numeric type stored in $id, in this case.
When you update this value by way of an increment operation (++), the [psobject] wrapper is lost, and the variable is updated with an unwrapped number (the original value + 1).
Therefore, starting with the 2nd iteration, $id contained an unwrapped [int] instance, resulting in the {0:x} number format being honored and therefore yielding a hexadecimal representation without a 0x prefix.
The only reason the 1st iteration yielded a 0x prefix was that it was present in the original string representation of the argument; as stated above, the numeric :x format specifier was ignored in this case, given that the -f operand was (unexpectedly) treated as a string.
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
How can I count the scale of a given decimal in Powershell?
$a = 0.0001
$b = 0.000001
Casting $a to a string and returning $a.Length gives a result of 6...I need 4.
I thought there'd be a decimal or math function but I haven't found it and messing with a string seems inelegant.
There's probably a better mathematic way but I'd find the decimal places like this:
$a = 0.0001
$decimalPlaces = ("$a" -split '\.')[-1].TrimEnd('0').Length
Basically, split the string on the . character and get the length of the last string in the array. Wrapping $a in double-quotes implicitly calls .ToString() with an invariant culture (you could expand this as $a.ToString([CultureInfo]::InvariantCulture)), making this method to determine the number of decimal places culture-invariant.
.TrimEnd('0') is used in case $a were sourced from a string, not a proper number type, it's possible that trailing zeroes could be included that should not count as decimal places. However, if you want the scale and not just the used decimal places, leave .TrimEnd('0') off like so:
$decimalPlaces = ("$a" -split '\.')[-1].Length
mclayton helpfully linked to this answer to a related C# question in a comment, and the solution there can indeed be adapted to PowerShell, if working with or conversion to type [decimal] is acceptable:
# Define $a as a [decimal] literal (suffix 'd')
# This internally records the scale (number of decimal places) as specified.
$a = 0.0001d
# [decimal]::GetBits() allows extraction of the scale from the
# the internal representation:
[decimal]::GetBits($a)[-1] -shr 16 -band 0xFF # -> 4, the number of decimal places
The System.Decimal.GetBits method returns an array of internal bit fields whose last element contains the scale in bits 16 - 23 (8 bits, even though the max. scale allowed is 28), which is what the above extracts.
Note: A PowerShell number literal that is a fractional number without the d suffix - e.g., 0.0001 becomes a [double] instance, i.e. a double-precision binary floating-point number.
PowerShell automatically converts [double] to [decimal] values on demand, but do note that there can be rounding errors due to the differing internal representations, and that [double] can store larger numbers than [decimal] can (although not accurately).
A [decimal] literal - one with suffix d (note that C# uses suffix m) - is parsed with a scale exactly as specified, so that applying the above to 0.000d and 0.010d yields 3 in both cases; that is, the trailing zeros are meaningful.
This does not apply if you (implicitly) convert from [double] instances such as 0.000 and 0.010, for which the above yields 0 and 2, respectively.
A string-based solution:
To offer a more concise (also culture-invariant) alternative to Bender The Greatest's helpful answer:
$a = 0.0001
("$a" -replace '.+\.').Length # -> 4, the number of decimal places
Caveat: This solution relies on the default string representation of a [double] number, which need not match the original input format; for instance, .0100, when stringified later, becomes '0.01'; however, as discussed above, you can preserve trailing zeros if you start with a [decimal] literal: .0100d stringifies to '0.0100' (input number of decimals preserved).
"$a", uses an expandable string (PowerShell's string interpolation) to create a culture-invariant string representation of the number so as to ensure that the string representation uses . as the decimal mark.
In effect, PowerShell calls $a.ToString([cultureinfo]::InvariantCulture) behind the scenes.[1].
By contrast, .ToString() (argument-less) applies the rules of the current culture, and in some cultures it is , - not . - that is used as the decimal mark.
Caveat: If you use just $a as the LHS of -replace, $a is implicitly stringified, in which case you - curiously - get culture-sensitive behavior, as with .ToString() - see this GitHub issue.
-replace '.+\.' effectively removes all characters up to and including the decimal point from the input string, and .Length counts the characters in the resulting string - the number of decimal places.
[1] Note that casts from strings in PowerShell too use the invariant culture (effectively, ::Parse($value, [cultureinfo]::InvariantCulture) is called) so that in order to parse a a culture-local string representation you'll need to use the ::Parse() method explicitly; e.g., [double]::Parse('1,2'), not [double] '1,2'.
Consider the following Powershell snippet:
[Uint64] $Memory = 1GB
[string] $MemoryFromString = "1GB"
[Uint64] $ConvertedMemory = [Convert]::ToUInt64($MemoryFromString)
The 3rd Line fails with:
Exception calling "ToUInt64" with "1" argument(s): "Input string was not in a correct format."
At line:1 char:1
+ [Uint64]$ConvertedMemory = [Convert]::ToUInt64($MemoryFromString)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : FormatException
If I check the contents of $Memory:
PS C:\> $Memory
1073741824
That works fine.
So, how do I convert the value "1GB" from a string to a UInt64 in Powershell?
To complement Sean's helpful answer:
It is only the type constraint of your result variable ([uint64] $ConvertedMemory = ...) that ensures that ($MemoryFromString / 1) is converted to [uint64] ([System.UInt64]).
The result of expression $MemoryFromString / 1 is actually of type [int] ([System.Int32]):
PS> ('1gb' / 1).GetType().FullName
System.Int32
Therefore, to ensure that the expression by itself returns an [uint64] instance, you'd have to use a cast:
PS> ([uint64] ('1gb' / 1)).GetType().FullName
System.Int64
Note the required (...) around the calculation, as the [uint64] cast would otherwise apply to '1gb' only (and therefore fail).
Alternatively, ('1gb' / [uint64] 1) works too.
Note:
'1gb' - 0 would have worked too,
but not '1gb' * 1' (effectively a no-op) or '1gb' + 0 (results in string '1gb0'), because operators * and + with a string-typed LHS perform string operations (replication and concatenation, respectively).
Automatic string-to-number conversion and number literals in PowerShell:
When PowerShell performs implicit number conversion, including when performing mixed-numeric-type calculations and parsing number literals in source code, it conveniently auto-selects a numeric type that is "large" enough to hold the result.
In implicit string-to-number conversions, PowerShell conveniently recognizes the same formats as supported in number literals in source code:
number-base prefixes (for integers only): 0x for hexadecimal integers, and 0b for binary integers (PowerShell [Core] 7.0+)
number-type suffixes: L for [long] ([System.Int64]), and D for [decimal] ([System.Decimal]); e.g., '1L' - 0 yields a [long].
Note that C# uses M instead of D and instead uses D to designate [System.Double]; also, C# supports several additional suffixes.
PowerShell [Core] 6.2+ now supports additional suffixes: Y ([sbyte]), UY ([byte]), S ([int16]), US ([uint16]), U ([uint32] or [uint64], on demand), and UL ([uint64]).
PowerShell [Core] 7.0+ additionally suports suffix n ([bigint])
You can keep an eye on future developments, if any, via the official help topic, about_Numeric_Literals.
floating-point representations such as 1.23 (decimal only); note that PowerShell only ever recognizes . as the decimal mark, irrespective of the current culture.
exponential notation (decimal only); e.g., '1.0e3' - 1 yields 999.
its own binary-multiplier suffixes, kb, mb, gb, tb, pb (for multipliers [math]::pow(2, 10) == 1024, [math]::pow(2, 20) == 1048576, ...); e.g., '1kb' - 1 yields 1023; note that theses suffixes are PowerShell-specific, so the .NET framework number-parsing methods do not recognize them.
The number-conversion rules are complex, but here are some key points:
This is based on my own experiments. Do tell me if I'm wrong.
Types are expressed by their PS type accelerators and map onto .NET types as follows:
[int] ... [System.Int32]
[long] ... [System.Int64]
[decimal] ... [System.Decimal]
[float] ... [System.Single]
[double] ... [System.Double]
PowerShell never auto-selects an unsigned integer type.
Note: In PowerShell [Core] 6.2+, you can use type suffix US, U or UL (see above) to force treatment as an unsigned type (positive number); e.g., 0xffffffffffffffffU
This can be unexpected with hexadecimal number literals; e.g., [uint32] 0xffffffff fails, because 0xffffffff is first - implicitly - converted to signed type [int32], which yields -1, which, as a signed value, cannot then be cast to unsigned type [uint32].
Workarounds:
Append L to force interpretation as an [int64] first, which results in expected positive value 4294967295, in which case the cast to [uint32] succeeds.
That technique doesn't work for values above 0x7fffffffffffffff ([long]::maxvalue), however, in which case you can use string conversion: [uint64] '0xffffffffffffffff'
PowerShell widens integer types as needed:
For decimal integer literals / strings, widening goes beyond integer types to [System.Decimal], and then [Double], as needed; e.g.:
(2147483648).GetType().Name yields Int64, because the value is [int32]::MaxValue + 1, and was therefore implicitly widened to [int64].
(9223372036854775808).GetType().Name yields Decimal, because the value is [int64]::MaxValue + 1, and was therefore implicitly widened to [decimal].
(79228162514264337593543950336).GetType().Name yields Double, because the value is [decimal]::MaxValue + 1, and was therefore implicitly widened to [double].
For hexadecimal (invariably integer) literals / strings, widening stops at [int64]:
(0x100000000).gettype().name yields Int64, because the value is [int32]::MaxValue + 1, and was therefore implicitly widened to [int64].
0x10000000000000000, which is [int64]::MaxValue + 1, does not get promoted to [System.Decimal] due to being hexadecimal and interpretation as a number therefore fails.
Note: The above rules apply to individual literals / strings, but widening in expressions may result in widening to [double] right away (without considering [decimal]) - see below.
PowerShell seemingly never auto-selects an integer type smaller than [int]:
('1' - 0).GetType().FullName yields System.Int32 (an [int]), even though integer 1 would fit into [int16] or even [byte].
The result of a calculation never uses a smaller type than either of the operands:
Both 1 + [long] 1 and [long] 1 + 1 yield a [long] (even though the result could fit into a smaller type).
Perhaps unexpectedly, PowerShell auto-selects floating-point type [double] for a calculation result that is larger than either operand's type integer type can fit, even if the result could fit into a larger integer type:
([int]::maxvalue + 1).GetType().FullName yields System.Double (a [double]), even though the result would fit into a [long] integer.
If one of the operands is a large-enough integer type, however, the result is of that type: ([int]::maxvalue + [long] 1).GetType().FullName yields System.Int64 (a [long]).
Involving at least one floating-point type in a calculation always results in [double], even when mixed with an integer type or using all-[float] operands:
1 / 1.0 and 1.0 / 1 and 1 / [float] 1 and [float] 1 / 1 and [float] 1 / [float] 1 all yield a [double]
Number literals in source code that don't use a type suffix:
Decimal integer literals are interpreted as the smallest of the following types that can fit the value: [int] > [long] > [decimal] > [double](!):
1 yields an [int] (as stated, [int] is the smallest auto-selected type)
214748364 (1 higher than [int]::maxvalue) yields a [long]
9223372036854775808 (1 higher than [long]::maxvalue) yields a [decimal]
79228162514264337593543950336 (1 higher than [decimal]::maxvalue) yields a [double]
Hexadecimal integer literals are interpreted as the smallest of the following types that can fit the value: [int] > [long]; that is, unlike with decimal literals, types larger than [long] aren't supported; Caveat: values that have the high bit set result in negative decimal numbers, because PowerShell auto-selects signed integer types:
0x1 yields an [int]
0x80000000 yields an [int] that is a negative value, because the high bit is set: -2147483648, which is the smallest [int] number, if you consider the sign ([int]::MinValue)
0x100000000 (1 more than can fit into an [int] (or [uint32])) yields a [long]
0x10000000000000000 (1 more than can fit into a [long] (or [uint64])) breaks, because [long] is the largest type supported ("the numeric constant is not valid").
To ensure that a hexadecimal literal results in a positive number:
Windows PowerShell: Use type suffix L to force interpretation as a [long] first, and then (optionally) cast to an unsigned type; e.g. [uint32] 0x80000000L yields 2147483648, but note that this technique only works up to 0x7fffffffffffffff, i.e., [long]::maxvalue; as suggested above, use a conversion from a string as a workaround (e.g., [uint64] '0xffffffffffffffff').
PowerShell [Core] 6.2+: Use type suffix us, u, or ul, as needed; e.g.: 0x8000us -> 32768 ([uint16]), 0x80000000u -> 2147483648 ([uint32]), 0x8000000000000000ul -> 9223372036854775808 ([uint64])
Binary integer literals (PowerShell [Core] 7.0+) are interpreted the same way as hexadecimal ones; e.g., 0b10000000000000000000000000000000 == 0x80000000 == -2147483648 ([int])
Floating-point or exponential notation literals (which are only recognized in decimal representation) are always interpreted as a [double], no matter how small:
1.0 and 1e0 both yield a [double]
Your problem is that the ToUint64 doesn't understand the Powershell syntax. You could get around it by doing:
($MemoryFromString / 1GB) * 1GB
As the $MemoryFromString will be converted its numeric value before the division.
This works because at the point of division Powershell attempts to convert the string to a number using its rules, rather than the .Net rules that are baked into ToUInt64. As part of the conversion if spots the GB suffix and applies it rules to expand the "1GB" string to 1073741824
EDIT: Or as PetSerAl pointed out, you can just do:
($MemoryFromString / 1)
What's the best way to round down to nearest whole number in PowerShell?
I am trying [math]::truncate but its not giving me predictable results.
Example:
$bla = 17.2/0.1
[math]::truncate($bla)
outputs 171 instead of the expected 172!
$bla = 172
[math]::truncate($bla)
outputs 172
I just need something that works.... and must always round down (i.e round($myNum + 0.5) won't work due to baker's rounding which may round up if the number has a 0.5 component).
Ah, I see. Looks like the datatype needs to be decimal:
[decimal] $de = 17.2/.1
[double] $db = 17.2/.1
[math]::floor($de)
172
[math]::floor($db)
171
http://msdn.microsoft.com/en-us/library/system.math.floor(v=vs.85).aspx
The Math::Floor function combined with [decimal] declaration should give you the results you want.
[Math]::Floor([decimal](17.27975/0.1))
returns = 172
The issue you are encountering with the original 17.2/0.1 division example is due to inaccuracy in the floating-point representation of the given decimal values (as mentioned in Joey's comment on another answer). You can see this in PowerShell by examining the round-trip representation of the final value:
PS> $bla = 17.2/0.1
PS> $bla.GetType().FullName
System.Double
PS> $bla.ToString()
172
PS> $bla.ToString('r')
171.99999999999997
A simple way to get around this is to declare the result as int, as PowerShell will automatically round to the the result to the nearest integer value:
PS> [int]$bli = 17.2/0.1
PS> $bli.GetType().FullName
System.Int32
PS> $bli.ToString()
172
Note that this uses the default .NET method of MidpointRounding.ToEven (also known as banker's rounding). This has nice statistical properties when tabulating large numbers of numeric values, but can also be changed to the simpler away-from-zero method:
function round( $value, [MidpointRounding]$mode = 'AwayFromZero' ) {
[Math]::Round( $value, $mode )
}
PS> [int]3.5
4
PS> [int]4.5
4
PS> round 3.5
4
PS> round 4.5
5
Another option is to use a more accurate representation for the original values, which will avoid the issue entirely:
PS> $bld = [decimal]17.2/0.1
PS> $bld.GetType().FullName
System.Decimal
PS> $bld.ToString()
172
[Math]::floor($x) is the built-in way to do it.
Just be aware of how it will behave with negative numbers. [Math]::floor(5.5) returns 5, but [Math]::floor(-5.5) returns -6.
If you need the function to return the value closest to zero, you'll need:
If ($x -ge 0) {
[Math]::Floor($x)
} Else {
[Math]::Ceiling($x)
}