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'.
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
Trying to convert the number numeric value in this string to a percent. Is there any easy way to do this in powershell?
"Percentage of records ","0.02"
So, the output would look like :
Percentage of records , 2%
Thanks in advance for any suggestions you can provide.
Yes, you can convert the string to a float data type (single, double, decimal), and then convert it back using a format string, like so:
"Percentage of records ", ([double]'0.02').ToString('P0')
And if you want it to output in a single line, you could use:
"Percentage of records: $(([double]'0.02').ToString('P0'))"
Explanation:
Convert your string to a float datatype: [double]'0.02'
Convert that float back into a string: .ToString()
But we want to format it as a percentage, so we supply P0 as a parameter.
i. P - means to format the value as a percentage, this performs the N * 100 operation for you and then adds on the percent sign
ii. 0 - controls the number of decimal places to show. In your case, you want to show zero decimal places.
Note: The percentage format string will round your value to the nearest decimal that you specify.
Example:
0.021.ToString('P0')
# returns 2%
0.025.ToString('P0')
# returns 3%
As #mklement0 pointed out in the comments. I hadn't considered that your sample may be a single string, like:
'"Percentage of records ","0.02"'
I assumed it was two strings, which you separated with a comma.
In the event it is a single string, then you need to extract the number to use it. Once you have isolated the number, then you can use my advice above:
$yourString = '"Percentage of records ","0.02"'
# probably the more "proper" way
$pctValue = ($yourString -split ',' -replace '"')[1]
# or
# a hacky way I just thought of that happens to work in this scenario
$pctValue = (iex $yourString)[1]
Explanation of first example:
-split ',' - Take the string, and break it out into multiple strings, separating them by comma
-replace '"','' - Replace all instances of " with blank. The second parameter is optional since you are removing. Could be written as -replace '"'
(...)[1] - This is saying to take the SECOND string that it returned (starts at zero). In this case it would be your 0.02 value.
Explanation of second example (this is a bit of a hack, but thought it would be fun to include anyway):
iex - alias for Invoke-Expression - it's telling powershell to run whatever is inside of the string verbatim. So it's the equivalent of typing "Percentage of records ","0.02" into powershell and hitting enter. Which in PowerShell terms, that is the equivalent of passing it a list of strings.
Use -f (format operator) in powerhsell for build your string :
"Percentage of records, {0:0%} " -f 0.02
or in percentage :
"Percentage of records, {0:P0} " -f 0.02
I am using Powershell and need to extract the digits before the decimal point so that I can evaluate the number extracted
So with $Interest_Rate = 15.5
I have tried the following code .. but they do not work:
$Interest_RatePart1 = "{0:N0}" -f $Interest_Rate
It rounds the value to 16
$Interest_RatePart1 = ($Interest_Rate -split '.')[0].trim()
It returns a blank.
I just want to return 15
Formatting the number will cause rounding away from zero
Use Math.Truncate() - which always rounds towards zero - instead:
$Interest_RatePart1 = [Math]::Truncate($Interest_Rate)
FWIW, the reason your last attempt returns nothing, is because -split defaults to regular expressions, and . means any character in regex.
Either escape the . with \:
$Interest_RatePart1 = ($Interest_Rate -split '\.')[0].Trim()
or specify that it shouldn't use regex:
$Interest_RatePart1 = ($Interest_Rate -split '.', 2, 'SimpleMatch')[0].Trim()
or use the String.Split() method instead:
$Interest_RatePart1 = "$Interest_Rate".Split('.')[0].Trim()
Mathias' [Math]::Truncate is correct - some other options for you though, pay attention to Floor as it is Slightly Different to Truncate when working with negative numbers.
Cast to int (can round up)
[int]$Interest_Rate
Use [Math]::Floor (will always round down, similar to truncate for non-negative numbers)
[Math]::Floor($Interest_Rate)
Use [Math]::Round with 0 decimal places. (can round up)
[Math]::Round($Interest_Rate, 0)
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)