3

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775
felixmc
  • 516
  • 1
  • 4
  • 19
  • 2
    This answer might help if you convert the C# code into PowerShell - https://stackoverflow.com/a/33490834/3156906. That deals with ```Decimal``` numbers though, not ```double``` which your example uses (```$b = 0.00001; write-host $b.GetType().FullName``` gives System.Double so I'm not sure how applicable it will be...) – mclayton Oct 29 '19 at 20:52
  • 1
    i agree that it seems a math method otta exist. it likely _does_ ...but i can't find it. however, your string technique will work with a bit more work try this ... >>> `([double]0.0001).ToString().Split('.')[-1].Length` <<< that gives me `4`. – Lee_Dailey Oct 29 '19 at 20:55
  • 1
    In case you're interested, [this](https://math.stackexchange.com/a/1212957) is the mathematic way to do it. – codewario Oct 29 '19 at 21:03
  • Good find, @mclayton, and good point that fractional number literals in PowerShell are `[double]` instances by default, but simply adding suffix `d` turns them into `[decimal]` (`0.00001d`); my answer now shows a PowerShell version of the linked C# solution. – mklement0 Oct 30 '19 at 19:22

2 Answers2

3

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
codewario
  • 19,553
  • 20
  • 90
  • 159
2

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'.

mklement0
  • 382,024
  • 64
  • 607
  • 775