How can I convert hexadecimal fraction to decimal in PowerShell? - powershell

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.

Related

Powershell number format

I am creating a script converting a csv file in an another format.
To do so, i need my numbers to have a fixed format to respect column size : 00000000000000000,00 (20 characters, 2 digits after comma)
I have tried to format the number with -f and the method $value.toString("#################.##") without success
Here is an example Input :
4000000
45817,43
400000
570425,02
15864155,69
1068635,69
128586256,9
8901900,04
29393,88
126858346,88
1190011,46
2358411,95
139594,82
13929,74
11516,85
55742,78
96722,57
21408,86
717,01
54930,49
391,13
2118,64
Any hints are welcome :)
Thank you !
tl;dr:
Use 0 instead of # in the format string:
PS> $value = 128586256.9; $value.ToString('00000000000000000000.00')
00000000000128586256.90
Note:
Alternatively, you could construct the format string as an expression:
$value.ToString('0' * 20 + '.00')
The resulting string reflects the current culture with respect to the decimal mark; e.g., with fr-FR (French) in effect, , rather than . would be used; you can pass a specific [cultureinfo] object as the second argument to control what culture is used for formatting; see the docs.
As in your question, I'm assuming that $value already contains a number, which implies that you've already converted the CSV column values - which are invariably strings - to numbers.
To convert a string culture-sensitively to a number, use [double]::Parse('1,2'), for instance (this method too has an overload that allows specifying what culture to use).
Caveat: By contrast, a PowerShell cast (e.g. [double] '1.2') is by design always culture-invariant and only recognizes . as the decimal mark, irrespective of the culture currently in effect.
zerocukor287 has provided the crucial pointer:
To unconditionally represent a digit in a formatted string and default to 0 in the absence of an available digit, use 0, the zero placeholder in a .NET custom numeric format string
By contrast, #, the digit placeholder, represents only digits actually present in the input number.
To illustrate the difference:
PS> (9.1).ToString('.##')
9.1 # only 1 decimal place available, nothing is output for the missing 2nd
PS> (9.1).ToString('.00')
9.10 # only 1 decimal place available, 0 is output for the missing 2nd
Since your input uses commas as decimal point, you can split on the comma and format the whole number and the decimal part separately.
Something like this:
$csv = #'
Item;Price
Item1;4000000
Item2;45817,43
Item3;400000
Item4;570425,02
Item5;15864155,69
Item6;1068635,69
Item7;128586256,9
Item8;8901900,04
Item9;29393,88
Item10;126858346,88
Item11;1190011,46
Item12;2358411,95
Item13;139594,82
Item14;13929,74
Item15;11516,85
Item16;55742,78
Item17;96722,57
Item18;21408,86
Item19;717,01
Item20;54930,49
Item21;391,13
Item22;2118,64
'# | ConvertFrom-Csv -Delimiter ';'
foreach ($item in $csv) {
$num,$dec = $item.Price -split ','
$item.Price = '{0:D20},{1:D2}' -f [int64]$num, [int]$dec
}
# show on screen
$csv
# output to (new) csv file
$csv | Export-Csv -Path 'D:\Test\formatted.csv' -Delimiter ';'
Output in screen:
Item Price
---- -----
Item1 00000000000004000000,00
Item2 00000000000000045817,43
Item3 00000000000000400000,00
Item4 00000000000000570425,02
Item5 00000000000015864155,69
Item6 00000000000001068635,69
Item7 00000000000128586256,09
Item8 00000000000008901900,04
Item9 00000000000000029393,88
Item10 00000000000126858346,88
Item11 00000000000001190011,46
Item12 00000000000002358411,95
Item13 00000000000000139594,82
Item14 00000000000000013929,74
Item15 00000000000000011516,85
Item16 00000000000000055742,78
Item17 00000000000000096722,57
Item18 00000000000000021408,86
Item19 00000000000000000717,01
Item20 00000000000000054930,49
Item21 00000000000000000391,13
Item22 00000000000000002118,64
I do things like this all the time, usually for generating computernames. That custom numeric format string reference will come in handy. If you want a literal period, you have to backslash it.
1..5 | % tostring 00000000000000000000.00
00000000000000000001.00
00000000000000000002.00
00000000000000000003.00
00000000000000000004.00
00000000000000000005.00
Adding commas to long numbers:
psdrive c | % free | % tostring '0,0' # or '#,#'
18,272,501,760
"Per mille" character ‰ :
.00354 | % tostring '#0.##‰'
3.54‰

PowerShell New-ADComputer binary attribute

I am new to PowerShell and I have a one shot task to perform which must be done with PowerShell. This involves Active Directory as well. I need to add a new computer object into our AD and one of the attributes I must set at creation time is a 16 bytes binary value. I am getting as input a string which is an hexadecimal representation of the value I must set for the attribute.
I tried to input the value asis and it doesn't work. I tried escaping each byte with a backslash, it doesn't work neither.
How should I format the input for this to work with the New-ADComputer command? I am setting a bunch of other attributes successfully. When I remove this binary entry from my hashtable passed to the -OtherAttributes option it works fine. So, obviously a format problem. I found nothing about the expected format for such attributes.
Any hints? TIA.
EDIT 2018-06-05 19:44 EDT:
I tried converting the string to a byte array as follow:
Function Convert-Hex2ByteArray {
[cmdletbinding()]
param(
[parameter(Mandatory=$true)]
[String]
$HexString
)
[byte[]] $Bytes = #(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)
For($i=0; $i -lt $HexString.Length; $i+=2) {
$Bytes[$i/2] = [convert]::ToByte($HexString.Substring($i, 2), 16)
}
$Bytes
}
(...)
$netbootGUID = Convert-Hex2ByteArray($args[$indiceArgs])
$otherAttributes.add( "netbootGUID", $netbootGUID )
(...)
New-ADComputer -Credential $cred -Server $ADhost -Path "CN=Computers,$baseDN" -SAMAccountName $sAMAccountName -Name $name-Instance 4 -OtherAttributes $otherAttributes
This leads to the following error (I apologize for my own translation since the original message is shown in French):
Many values were specified for an attribut which can only have one
Problem solved:
$netbootGUID = New-Object Guid $args[$indiceArgs]
$otherAttributs.add( "netbootGUID", $netbootGUID )
Did the trick.
Typically for binary storage you need to convert the string to a byte array:
$String = '3c6ef75eaa2c4b23992bbd65ac891917'
$ByteArray = [byte[]]$(for ($i = 0; $i -lt $String.Length; $i+=2) { [Convert]::ToByte($String.Substring($i,2), 16) })
To convert it back:
$NewString = -join $(foreach($Byte in $ByteArray) { $Byte.ToString('x2') })
If you want the characters upper case, specify 'X2' instead of 'x2'.
Since you're storing 16 byte values, I'll note that if you're storing GUIDs you may need to change the storage order since the order of bytes in a string representation of a GUID does not match the order of bytes in a byte representation of a GUID on an x86 system. Fortunately, there are built in functions for handling this conversion with the built-in System.Guid data type:
$GUID = 'f8d89eb2b49c4bfeab44a85ccdc4191a'
$ByteArray = [Guid]::new($GUID).ToByteArray()
And a constructor for converting back:
$NewGUID = [Guid]::new($ByteArray)
Whether or not you should use this method depends on exactly what property you're updating and whether or not the application(s) that will be using the property in question will correctly be handling the GUIDs or if they're just storing the GUID as raw bytes (which is incorrect but not surprising). You'll have to test by seeing what GUID your application sees and comparing it to the byte array in Active Directory to verify that it's correct.
For specifics on the byte ordering, see the documentation for Guid.ToByteArray():
Note that the order of bytes in the returned byte array is different from the string representation of a Guid value. The order of the beginning four-byte group and the next two two-byte groups is reversed, whereas the order of the last two-byte group and the closing six-byte group is the same. The example provides an illustration.
The reason for this is that a GUID is partially constructed from a series of integers of varying sizes, and the UUID standard specifies big endianness for those numbers. x86 computers are little-endian systems.

Powershell: Convert unique string to unique int

Is there a method for converting unique strings to unique integers in PowerShell?
I'm using a PowerShell function as a service bus between two API's,
the first API produces unique codes e.g. HG44X10999 (varchars)- but the second API which will consume the first as input, will only accept integers. I only care about keeping them unique.
I have looked at $string.gethashcode() but this produces negative integers and also changes between builds. Get-hash | $string -encoding ASCII obviously outputs varchars too.
Other examples on SO are referring to converting a string of numeric characters to integers i.e. $string = 123 - but I can't find a way of quickly computing an int from a string of alphanumeric
The Fowler-Noll-Vo hash function seems well-suited for your purpose, as it can produce a 32-bit hash output.
Here's a simple implementation in PowerShell (the offset basis and initial prime is taken from the wikipedia reference table for 32-bit outputs):
function Get-FNVHash {
param(
[string]$InputString
)
# Initial prime and offset chosen for 32-bit output
# See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function
[uint32]$FNVPrime = 16777619
[uint32]$offset = 2166136261
# Convert string to byte array, may want to change based on input collation
$bytes = [System.Text.Encoding]::UTF8.GetBytes($InputString)
# Copy offset as initial hash value
[uint32]$hash = $offset
foreach($octet in $bytes)
{
# Apply XOR, multiply by prime and mod with max output size
$hash = $hash -bxor $octet
$hash = $hash * $FNVPrime % [System.Math]::Pow(2,32)
}
return $hash
}
Now you can repeatably produce distinct integers from the input strings:
PS C:\> Get-FNVHash HG44X10999
1174154724
If the target API only accepts positive signed 32-bit integers you can change the modulus to [System.Math]::Pow(2,31) (doubling the chance of collisions, to
approx. 1 in 4300 for 1000 distinct inputs)
For further insight into this simple approach, see this page on FNV and have a look at this article exploring short string hashing

Powershell - Hex to IEEE754 Single-Precision Floating-Point

I'm new in Powershell and I'm struggling with a basic conversion from hex to float.
I'd like to convert 0x46FEEBD0 to float number 32629.90625 (IEEE754 standard).
All my attempts gave me 1191111632 (or 1,191112E+09) which is the decimal representation of 0x46FEEBD0.
Is there a simple Powershell method to do so ?
Thanks
Yes, you can use the BitConverter.ToSingle() method:
# Get the byte representation (produces D0, EB, FE, 46)
$bytes = [BitConverter]::GetBytes([single]32629.90625)
$bytes | Foreach-Object { ("{0:X2}" -f $_) }
# Convert the $bytes back to a float (produces 32629.91)
[BitConverter]::ToSingle($bytes, 0)
Edit: Thanks to LotPings for pointing out that I should be using single rather than double.

Powershell - Round down to nearest whole number

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)
}