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'.
Is there a way to convert hexadecimal fractions (i.e.: numbers with values <1.0, represented in hex) into their decimal equivalents in PowerShell?
Examples:
Hex Dec
0.0858b9da24fb4cac = 0.032603851087498366
0.8c3115559ab0c10b = 0.5476239522928976
Guides I've found for general HEX/DEC conversion say to use [Convert], which is fine for whole numbers, but it seems to completely fail when I throw a decimal point in the string.
Note: Examples were taken from data on another site, where these calculations are routinely done. I haven't fully verified their accuracy - there may be some error in the last few digits. Fortunately, I won't be using that much precision anyway.
I don't know of a builtin .Net way to do it. I wrote this brute-force converter, but your values don't seem to exactly match up with .Net type precision - [double] (64bit) is too low precision, and [decimal] (128bit) is more.
Your examples come out like so:
0.0858b9da24fb4cac = 0.032603851087498366
= 0.0326038510874983682928164128
0.8c3115559ab0c10b = 0.5476239522928976
= 0.5476239522928976344718082711
Code:
<#
.Synopsis
Converts strings containing hexadecimal numbers with fractional components
into base-10 [decimal] numbers.
.EXAMPLE
PS C:\> ConvertFrom-HexFraction '0.0858b9da24fb4cac'
0.0326038510874983682928164128
.Example
PS C:\> '0.0858b9da24fb4cac', '0.8c3115559ab0c10b' | ConvertFrom-HexFraction
0.0326038510874983682928164128
0.5476239522928976344718082711
#>
function ConvertFrom-HexFraction
{
[CmdletBinding()]
[OutputType([decimal])]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipeline=$true)]
[string]$hexValue
)
Process
{
# strip leading hex indicator, setup result placeholder of desired type.
$hexValue = $hexValue -replace '^(0x|&h)'
$result = [decimal]0
# Loop over the digits (hexits?) in the string, skip the (hexa)decimal point '.'
# and calculate (value * base ^ columnNum) for each.
$i = $hexValue.IndexOf('.') - 1
[char[]]$hexValue | Where { $_ -ne '.' } | ForEach {
$result += [convert]::ToInt32($_, 16) * [Math]::Pow(16, $i--)
}
$result #output the result
}
}
ConvertFrom-HexFraction '0.0'
ConvertFrom-HexFraction 'a.a'
ConvertFrom-HexFraction '0.0858b9da24fb4cac'
ConvertFrom-HexFraction '0.8c3115559ab0c10b'
'0.0858b9da24fb4cac', '0.8c3115559ab0c10b' | ConvertFrom-HexFraction
Ok, this one caught my interest. I didn't even know that hexadecimal fractions were a thing, so I went and looked it up. Converting them isn't covered in the [math] or [convert] libraries that I can see, so we'll have to write up a function that takes care of it for us. I wrote this up, and it's relatively accurate, but you have to consider that a fraction as small as you have it there's going to be some rounding... I mean really, for your first example even you rounded. The last digit alone is something like 6.50521303491303E-19 (which is 12*(16^-16)).
So, that function looks like:
Function HexToInt([string]$TextIn){
$Pre,$Post = $TextIn.split('.')
$PostConverted = For($i=1;$i -le $Post.Length;$i++){
[convert]::ToInt32($Post[($i-1)],16) * [math]::pow(16,($i*-1))
}
$PostSum = $PostConverted | Measure-Object -Sum |% Sum
$Int = [convert]::ToInt64($Pre,16)
$Int+$PostSum
}
That should convert your fractions for you.
I am trying to write to a multi string, but using data gleaned from a REG file, so it's in Hex format. I have managed to convert the string to a byte array using the Convert-HexStringToByteArray here, but that doesn't produce the same result in the registry as loading the REG, so I am thinking that is not actually the right data type to be casting to.
The initial data looks like this
"NavigatorLayoutOrder"=hex(7):31,00,30,00,00,00,31,00,00,00,32,00,00,00,33,00,00,00,30,00,00,00,34,00,00,00,35,00,00,00,36,00,00,00,37,00,00,00,38,00,00,00,39,00,00,00,31,00,31,00,00,00,31,00,32,00,00,00,31,00,33,00,00,00,31,00,34,00,00,00,31,00,35,00,00,00,31,00,36,00,00,00,31,00,37,00,00,00,31,00,38,00,00,00,31,00,39,00,00,00,32,00,30,00,00,00,32,00,31,00,00,00,32,00,32,00,00,00,00,00
and I have removed the hex(7): off the front, then tried it as a pure string and casting to a byte array, and neither seems to work.
I have found reference to REG_MULTI_SZ being UTF-16le, but my understanding is that this is also the default for PowerShell, so I shouldn't need to be changing the encoding, but perhaps I am wrong there?
EDIT: I also tried this, with again a successful write, but the wrong result.
$enc = [system.Text.Encoding]::UTF8
[byte[]]$bytes = $enc.GetBytes($string)
Also tried
$array = $string.Split(',')
$byte = [byte[]]$array
This also puts data into the registry, but the result is not the same as importing the REG. And, everything I am finding keeps pointing at the idea that the REG file is UTF16, so I tried
$enc = [system.Text.Encoding]::Unicode
[byte[]]$bytes = $enc.GetBytes($string)
both with BigEndianUnicode & Unicode. Not only did it not work, the result is the same which I find odd. Seems like changing the endian-ness SHOULD change the result.
EDIT: To clarify, the input string as taken from the REG file is shown above. I simply removed the hex(7): from the front of the data.
The results are seen here, where the second value is what results from PowerShell, while the first is what the REG file produced.
The code used to produce this was
$string = "31,00,30,00,00,00,31,00,00,00,32,00,00,00,33,00,00,00,30,00,00,00,34,00,00,00,35,00,00,00,36,00,00,00,37,00,00,00,38,00,00,00,39,00,00,00,31,00,31,00,00,00,31,00,32,00,00,00,31,00,33,00,00,00,31,00,34,00,00,00,31,00,35,00,00,00,31,00,36,00,00,00,31,00,37,00,00,00,31,00,38,00,00,00,31,00,39,00,00,00,32,00,30,00,00,00,32,00,31,00,00,00,32,00,32,00,00,00,00,00"
$enc = [system.Text.Encoding]::BigEndianUnicode
[byte[]]$bytes = $enc.GetBytes($string)
New-ItemProperty "HKCU:\Software\Synchro\Synchro\ProjectConfig" -name:"NavigatorLayoutOrder2" -value:$bytes -propertyType:MultiString -force
Using Unicode encoding produces a very slightly different, but still wrong, result.
For one thing, the multistring is little endian encoded, so you need [Text.Encoding]::Unicode, not [Text.Encoding]::BigEndianUnicode. Plus, using [Text.Encoding]::Unicode.GetBytes() on the string from the .reg file ("31,00,30,00,...") would give you a byte array of the characters of that string:
'3' → 51, 0
'1' → 49, 0
',' → 44, 0
'0' → 48, 0
…
What you actually want is a byte array of the comma-separated hexadecimal values in that string:
31 → 49 (character '1')
00 → 0 (character NUL)
30 → 48 (character '0')
…
Split the string at commas, convert the hexadecimal number strings to integers, and cast the resulting list of integers to a byte array:
[byte[]]$bytes = $string -split ',' | ForEach-Object { [int]"0x$_" }
Then you can convert that (little endian encoded) byte array to a string:
$ms = [Text.Encoding]::Unicode.GetString($bytes)
and write that to the registry:
$key = 'HKCU:\Software\Synchro\Synchro\ProjectConfig'
$name = 'NavigatorLayoutOrder2'
New-ItemProperty $key -Name $name -Value $ms -PropertyType MultiString -Force