3
\$\begingroup\$

This is a fully functional PowerShell script I wrote not long ago that converts data (numerical data) among 4 data formats: binary, decimal, hexadecimal and ipv4.

It supports 12 conversions, it can convert number from each type to the other three type, it can convert:

binary to decimal      | binary to hexadecimal  | binary to ipv4
decimal to binary      | decimal to hexadecimal | decimal to ipv4
hexadecimal to binary  | hexadecimal to decimal | hexadecimal to ipv4
ipv4 to binary         | ipv4 to decimal        | ipv4 to hexadecimal

And I have tested the script numerous times and confirmed it gives right answers, I wrote the script all by myself without any help from anyone and without using the inbuilt methods of PowerShell, of course I know these .Net methods and I just don't use them in my script because that wouldn't be a programming challenge and won't help me improve my skills, though of course I have checked the results of my script against those of the built-in methods and found no discrepancies.

So here is the script:

function NumConversion {

    param(
        [parameter(valuefrompipeline=$true, mandatory=$true, position=0)] [ValidateNotNull()] [string] $data,
        [parameter(valuefrompipeline=$true, mandatory=$true, position=1)] [ValidateNotNull()] [string] $task
    )
    
    begin {
        
        $binhex = @{
            '0000' = '0'
            '0001' = '1'
            '0010' = '2'
            '0011' = '3'
            '0100' = '4'
            '0101' = '5'
            '0110' = '6'
            '0111' = '7'
            '1000' = '8'
            '1001' = '9'
            '1010' = 'a'
            '1011' = 'b'
            '1100' = 'c'
            '1101' = 'd'
            '1110' = 'e'
            '1111' = 'f'
        }
        
        $dechex = @{
            '0' = '0'
            '1' = '1'
            '2' = '2'
            '3' = '3'
            '4' = '4'
            '5' = '5'
            '6' = '6'
            '7' = '7'
            '8' = '8'
            '9' = '9'
            '10' = 'a'
            '11' = 'b'
            '12' = 'c'
            '13' = 'd'
            '14' = 'e'
            '15' = 'f'
        }
    }
    
    process {
    
        $bin2dec = {
        
            param($bin)
            
            $power = 1
            $dec = 0
            [int[]]$bits = $bin -split "(0|1)" | where {$_ -ne ""}
            for ($i = $bits.count - 1; $i -ge 0; $i--) {
                $bit = $bits[$i]
                $dec += $bit * $power
                $power *= 2
            }
            $dec
        }
        
        $bin2hex = {
        
            param($bin)
            
            $i = 0
            if ($bin.Length % 4 -ne 0) {
                while ($i -lt $bin.Length) {$i += 4}
                $bin = '0' * ($i - $bin.Length) + $bin
            }
            $bin = $bin -split '([01]{4})' | where {$_ -ne ''}
            $hex = ($bin | % { $binhex.$_ }) -join ''
            $hex
        }
        
        $bin2ip = {
        
            param($bin)
            
            if ($bin.length -lt 32) { $bin = '0' * (32 - $bin.length) + $bin }
            [array]$bin = $bin -split '([01]{8})' | where {$_ -ne ''}
            [array]$ip = foreach($seg in $bin) { &$bin2dec -bin $seg }
            $ip = $ip -join '.'
            $ip
        }
        
        $dec2bin = {
        
            param($dec)
            
            [array]$bits = for ($i = 1; $i -le $dec; $i*=2) {$i}
            [string[]]$bin = for($i = $bits.count - 1; $i -ge 0; $i--) {
                $bit = $bits[$i]
                if ($dec -ge [double]$bit) {1; $dec -= $bit}
                else {0}
            }
            [string]$bin = $bin -join ''
            $bin
        }
        
        $dec2hex = {
        
            param($dec)
            
            [array]$bits = for ($i = 1; $i -le $dec; $i *= 16) {$i}
            [string[]]$hex = for ($i = $bits.count - 1; $i -ge 0; $i--) {
                $bit = $bits[$i]
                if ($dec -ge $bit) {
                    $num = [math]::floor($dec / $bit)
                    $dec = $dec % $bit
                    $dechex.([string]$num)
                }
                else {0}
            }
            [string]$hex = $hex -join ""
            $hex
        }

        $dec2ip = {
        
            param($dec)
            
            [array]$ip = for ($i = 0; $i -le 3; $i++) {
                $seg = $dec % 256
                $dec = ($dec - $seg) / 256
                $seg
            }
            $ip = for ($i = 3; $i -ge 0; $i--) { $ip[$i] }
            $ip = $ip -join '.'
            $ip
        }
        
        $hex2bin = {
        
            param($hex)
            
            [array]$hexa = $hex -split '(\w{1})' | where {$_ -ne ''}
            $bin = for ($i = 0; $i -lt $hexa.count; $i++) {
                $binhex.keys | where {$binhex.$_ -eq $hexa[$i]}
            }
            $bin = $bin -join ''
            $bin
        }
        
        $hex2dec = {
        
            param($hex)
            
            [array]$bits = for($i = 0; $i -lt $hex.length; $i++){[math]::pow(16, $i)}
            $dec = 0
            $hexa = $hex -split '(\w{1})' | where {$_ -ne ''}
            for (($i = 0), ($j = $hexa.count - 1); $i -lt $hexa.count; $i++, $j--) {
                $num = [double]($dechex.keys | where {$dechex.$_ -eq $hexa[$j]})
                $bit = $bits[$i]
                $dec = $dec + $num * $bit
            }
            $dec
        }
        
        $hex2ip = {
        
            param($hex)
            
            if ($hex.length -lt 8) {$hex = '0' * (8 - $hex.length) + $hex }
            [array]$hexa = $hex -split '(\w{2})' | where {$_ -ne ''}
            [array]$ip = foreach($seg in $hexa) { &$hex2dec -hex $seg }
            $ip = $ip -join '.'
            $ip
        }
        
        $ip2bin = {
        
            param($ip)
            
            $ip = $ip.split('.')
            [array]$bin = foreach ($seg in $ip) {
                [int]$num = $seg
                [array]$bits = for ($i = 128; $i -ge 1; $i /= 2) {$i}
                $bits = $bits | % {
                    if ($num -ge [int64]$_) {1; $num -= $_}
                    else {0} 
                }
                [string]$bits = $bits -join ""
                $bits
            }
            $bin = $bin -join ''
            $bin
        }
        
        $ip2dec = {
        
            param($ip)
            
            $ip = $ip.split('.')
            $dec = 0
            $base = 16777216
            foreach ($seg in $ip) {
                $dec += [int]$seg * $base
                $base = $base / 256
            }
            $dec
        }
        
        $ip2hex = {
        
            param($ip)
            
            $ip = $ip.split('.')
            [string[]]$hex = foreach ($seg in $ip) {
                $byte = &$dec2hex -dec $seg
                if ($byte.length -lt 2) { $byte = '0' + $byte }
                $byte
            }
            $hex = $hex -join ''
            $hex
        }
        
        switch ($task)
        {
            'bd' { [string]$bin = $data; &$bin2dec -bin $bin }
            'bh' { [string]$bin = $data; &$bin2hex -bin $bin }
            'bi' { [string]$bin = $data; &$bin2ip -bin $bin }
            'db' { [double]$dec = $data; &$dec2bin -dec $dec }
            'dh' { [double]$dec = $data; &$dec2hex -dec $dec }
            'di' { [double]$dec = $data; &$dec2ip -dec $dec }
            'hb' { [string]$hex = $data; &$hex2bin -hex $hex }
            'hd' { [string]$hex = $data; &$hex2dec -hex $hex }
            'hi' { [string]$hex = $data; &$hex2ip -hex $hex }
            'ib' { [string]$ip = $data; &$ip2bin -ip $ip }
            'id' { [string]$ip = $data; &$ip2dec -ip $ip }
            'ih' { [string]$ip = $data; &$ip2hex -ip $ip }
        }
    }
}
$data = $Args[0]
$task = $Args[1]
NumConversion $data $task

I came up with the logics and the algorithms all by myself;

Usage:

Save the script as .\NumConversion.ps1, it uses 12 keywords to identify which process to carry out, the 12 switches are (in the order of the table shown above):

bd | bh | bi
db | dh | di
hb | hd | hi
ib | id | ih

Now if you want to convert something you just give it two arguments, the data as the first argument and the keyword as the second argument, for example, if you want to convert the ipv4 address 127.0.0.1 to binary:

.\NumConversion.ps1 127.0.0.1 ib

And it will return:

01111111000000000000000000000001

I want your thoughts on my script, can it be more performant, how can it be improved? Can it be shorter and still be clear? Can the code be simplified? I am still very new to all these programming thing, and I want to know how my skills can be improved. Thank you.


I will briefly explain the logic I use in these algorithms.

I found 4 binary bits equal to 1 hexadecimal bit, namely 4 binary bits can represent numbers from 0 to 15, or 2^4 - 1, and integers from 0 to 15 can be represented by 1 hexadecimal bit, and there can be one to one correspondence between every four binary bits and one hexadecimal bit, and binary numbers can be just split into chunks of four (if the string is padded into multiples of four) and translated into hexadecimal bits, because every 4 binary bits segment has the base unit 16 times the previous unit, exactly like 1 hexadecimal bit.

And I think IPv4 is sort of base-256, because from left to right, each consecutive segment has the base 256 times of the previous segment, I understand IPv4 is a 4-byte number and 1 byte equals to 8 binary bits and 2 hexadecimal bits, because 8 binary bits can represent integers 0 - 255 (2^8 - 1), and 2 hexadecimal bits can also represent integers 0 - 255 (16^2 - 1), so there is a one-one correspondence among 1 byte of ipv4 and 2 bits of hexadecimal and 8 bits of binary, and I can just split the string (if it is padded first) and translate the value.

I mainly use split and padding, I split the binary/hexadecimal/ipv4 string into chunks of appropriate size and store the values in an array, then translate the segments one by one and join strings.

The codes to convert binary, hexadecimal and ipv4 are similar, I wonder if I can use one script block to achieve the conversion processes and use a parameter to determine which conversion to run.

I wonder if I can use less code to get the same results using the same logic.


Update: Yeah, thanks for JosefZ's feedback, the two problem are now solved in my new edit.

About the first issue, that was unexpected and unintended, and that was definitely a bug, and I was able to fix it by explicitly declare the data type of the variable $dec to appropriate data type; Now I have improved the type casting of all the variables, the bug is fixed.

And about the second issue, that was definitely not a bug, however I definitely haven't intended for it to even give an output, it was working as it should be.

You all know that saying: garbage in, garbage out; you inputted wrong data, data that the algorithm isn't designed to process, you must expect it to give wrong result.

I thought everyone that will be using my script will be in their clear minds and won't input the wrong data to the script, so I didn't implement exception handling functions.

I was lazy, now I have fixed it, the script will now through out 7 error messages depending on 7 possible situations, the process won't run if you don't input correctly formatted data in one of the four data types possible respective to the process;

And if you use one of the three functions that converts data into IPv4, and the value exceeds the maximum possible value (11111111111111111111111111111111 for binary, 4294967295 for decimal and ffffffff for hexadecimal), the process won't run.

Now everything is definitely working as intended, but there are even more duplicate lines of code that can be improved, but I am working on other projects now and I don't have time to improve the code.

The updated code:

function NumConversion {

    param(
        [parameter(valuefrompipeline=$true, mandatory=$true, position=0)] [ValidateNotNull()] [string] $data,
        [parameter(valuefrompipeline=$true, mandatory=$true, position=1)] [ValidateNotNull()] [string] $task
    )
    
    begin {
        
        $binhex = @{
            '0000' = '0'
            '0001' = '1'
            '0010' = '2'
            '0011' = '3'
            '0100' = '4'
            '0101' = '5'
            '0110' = '6'
            '0111' = '7'
            '1000' = '8'
            '1001' = '9'
            '1010' = 'a'
            '1011' = 'b'
            '1100' = 'c'
            '1101' = 'd'
            '1110' = 'e'
            '1111' = 'f'
        }
        
        $dechex = @{
            '0' = '0'
            '1' = '1'
            '2' = '2'
            '3' = '3'
            '4' = '4'
            '5' = '5'
            '6' = '6'
            '7' = '7'
            '8' = '8'
            '9' = '9'
            '10' = 'a'
            '11' = 'b'
            '12' = 'c'
            '13' = 'd'
            '14' = 'e'
            '15' = 'f'
        }
    }
    
    process {
    
        $bin2dec = {
        
            param($bin)
            
            [string]$bin = $bin
            if ($bin -match "[01]{$($bin.length)}") {
                $power = 1
                $dec = 0
                [int[]]$bits = $bin -split "(0|1)" | where {$_ -ne ""}
                for ($i = $bits.count - 1; $i -ge 0; $i--) {
                    $bit = $bits[$i]
                    $dec += $bit * $power
                    $power *= 2
                }
                $dec
            } else { write-error -message "InvalidData: The inputted string isn't a valid binary string, process will now stop." -Category InvalidData; break }
        }
        
        $bin2hex = {
        
            param($bin)
            
            [string]$bin = $bin
            if ($bin -match "[01]{$($bin.length)}") {
                $i = 0
                if ($bin.Length % 4 -ne 0) {
                    while ($i -lt $bin.Length) {$i += 4}
                    $bin = '0' * ($i - $bin.Length) + $bin
                }
                [array]$bin = $bin -split '([01]{4})' | where {$_ -ne ''}
                $hex = ($bin | % { $binhex.$_ }) -join ''
                $hex
            } else { write-error -message "InvalidData: The inputted string isn't a valid binary string, process will now stop." -Category InvalidData; break }
        }
        
        $bin2ip = {
        
            param($bin)
            
            [string]$bin = $bin
            if ($bin -match "[01]{$($bin.length)}") {
                if ($bin -eq ($bin | select-string -pattern "[01]{1,32}").matches.value) {
                    if ($bin.length -lt 32) { $bin = '0' * (32 - $bin.length) + $bin }
                    [array]$bin = $bin -split '([01]{8})' | where {$_ -ne ''}
                    [array]$ip = foreach($seg in $bin) { &$bin2dec -bin $seg }
                    $ip = $ip -join '.'
                    $ip
                } else { write-error -message "LimitsExceeded: The value of the inputted binary string exceeds the maximum IPv4 value possible, process will now stop.(maximum value allowed: 11111111111111111111111111111111)" -Category LimitsExceeded; break }
            } else { write-error -message "InvalidData: The inputted string isn't a valid binary string, process will now stop." -Category InvalidData; break }
        }
        
        $dec2bin = {
        
            param($dec)
            
            if ([string]$dec -match "[0-9]{$($([string]$dec).Length)}") {
                [double]$dec = $dec
                [array]$bits = for ($i = 1; $i -le $dec; $i*=2) {$i}
                [string[]]$bin = for($i = $bits.count - 1; $i -ge 0; $i--) {
                    $bit = $bits[$i]
                    if ($dec -ge [double]$bit) {1; $dec -= $bit}
                    else {0}
                }
                [string]$bin = $bin -join ''
                $bin
            } else { write-error -message "InvalidData: The inputted string isn't a valid binary string, process will now stop." -Category InvalidData; break }
        }
        
        $dec2hex = {
        
            param($dec)
            
            if ([string]$dec -match "[0-9]{$($([string]$dec).Length)}") {
                [double]$dec = $dec
                [array]$bits = for ($i = 1; $i -le $dec; $i *= 16) {$i}
                [string[]]$hex = for ($i = $bits.count - 1; $i -ge 0; $i--) {
                    $bit = $bits[$i]
                    if ($dec -ge $bit) {
                        $num = [math]::floor($dec / $bit)
                        $dec = $dec % $bit
                        $dechex.([string]$num)
                    }
                    else {0}
                }
                [string]$hex = $hex -join ""
                $hex
            } else { write-error -message "InvalidData: The inputted string isn't a valid decimal natural number, process will now stop." -Category InvalidData; break }
        }

        $dec2ip = {
        
            param($dec)
            
            if ([string]$dec -match "[0-9]{$($([string]$dec).Length)}") {
                [int64]$dec = $dec
                if ($dec -le 4294967295) {
                    [array]$ip = for ($i = 0; $i -le 3; $i++) {
                        $seg = $dec % 256
                        $dec = ($dec - $seg) / 256
                        $seg
                    }
                    $ip = for ($i = 3; $i -ge 0; $i--) { $ip[$i] }
                    $ip = $ip -join '.'
                    $ip
                } else { write-error -message "LimitsExceeded: The value of the inputted decimal natural number exceeds the maximum IPv4 value possible, process will now stop. (maximum value allowed: 4294967295)" -Category LimitsExceeded; break }
            } else { write-error -message "InvalidData: The inputted string isn't a valid decimal natural number, process will now stop." -Category InvalidData; break }
        }
        
        $hex2bin = {
        
            param($hex)
            
            [string]$hex = $hex
            if ($hex -match "[0-9a-f]{$($hex.length)}") {
                [array]$hexa = $hex -split '(\w{1})' | where {$_ -ne ''}
                $bin = for ($i = 0; $i -lt $hexa.count; $i++) {
                    $binhex.keys | where {$binhex.$_ -eq $hexa[$i]}
                }
                $bin = $bin -join ''
                $bin
            } else { write-error -message "InvalidData: The inputted string isn't a valid hexadecimal number, process will now stop." -Category InvalidData; break }
        }
        
        $hex2dec = {
        
            param($hex)
            
            [string]$hex = $hex
            if ($hex -match "[0-9a-f]{$($hex.length)}") {
                [array]$bits = for($i = 0; $i -lt $hex.length; $i++){[math]::pow(16, $i)}
                $dec = 0
                [array]$hexa = $hex -split '(\w{1})' | where {$_ -ne ''}
                for (($i = 0), ($j = $hexa.count - 1); $i -lt $hexa.count; $i++, $j--) {
                    $num = [double]($dechex.keys | where {$dechex.$_ -eq $hexa[$j]})
                    $bit = $bits[$i]
                    $dec = $dec + $num * $bit
                }
                $dec
            } else { write-error -message "InvalidData: The inputted string isn't a valid hexadecimal number, process will now stop." -Category InvalidData; break }
        }
        
        $hex2ip = {
        
            param($hex)
            
            [string]$hex = $hex
            if ($hex -match "[0-9a-f]{$($hex.length)}") {
                if ($hex -eq ($hex | select-string -pattern "[0-9a-f]{1,8}").matches.value) {
                    if ($hex.length -lt 8) {$hex = '0' * (8 - $hex.length) + $hex }
                    [array]$hexa = $hex -split '(\w{2})' | where {$_ -ne ''}
                    [array]$ip = foreach($seg in $hexa) { &$hex2dec -hex $seg }
                    $ip = $ip -join '.'
                    $ip
                } else { write-error -message "LimitsExceeded: The value of the inputted hexadecimal number exceeds the maximum IPv4 value possible, process will now stop. (maximum value allowed: ffffffff)" -Category LimitsExceeded; break }
            } else { write-error -message "InvalidData: The inputted string isn't a valid hexadecimal number, process will now stop." -Category InvalidData; break }
        }
        
        $ip2bin = {
        
            param($ip)
            
            [string]$ip = $ip
            if ($ip -match '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)') {
                [array]$ip = $ip.split('.')
                [array]$bin = foreach ($seg in $ip) {
                    [int]$num = $seg
                    [array]$bits = for ($i = 128; $i -ge 1; $i /= 2) {$i}
                    $bits = $bits | % {
                        if ($num -ge [int64]$_) {1; $num -= $_}
                        else {0} 
                    }
                    [string]$bits = $bits -join ""
                    $bits
                }
                $bin = $bin -join ''
                $bin
            } else { write-error -message "InvalidData: The inputted string isn't a valid IPv4 string, process will now stop." -Category InvalidData; break }
        }
        
        $ip2dec = {
        
            param($ip)
            
            [string]$ip = $ip
            if ($ip -match '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)') {
                [array]$ip = $ip.split('.')
                $dec = 0
                $base = 16777216
                foreach ($seg in $ip) {
                    $dec += [int]$seg * $base
                    $base = $base / 256
                }
                $dec
            } else { write-error -message "InvalidData: The inputted string isn't a valid IPv4 string, process will now stop." -Category InvalidData; break }
        }
        
        $ip2hex = {
        
            param($ip)
            
            [string]$ip = $ip
            if ($ip -match '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)') {
                [array]$ip = $ip.split('.')
                [string[]]$hex = foreach ($seg in $ip) {
                    $byte = &$dec2hex -dec $seg
                    if ($byte.length -lt 2) { $byte = '0' + $byte }
                    $byte
                }
                $hex = $hex -join ''
                $hex
            } else { write-error -message "InvalidData: The inputted string isn't a valid IPv4 string, process will now stop." -Category InvalidData; break }
        }
        
        switch ($task)
        {
            'bd' { $bin = $data; &$bin2dec -bin $bin }
            'bh' { $bin = $data; &$bin2hex -bin $bin }
            'bi' { $bin = $data; &$bin2ip -bin $bin }
            'db' { $dec = $data; &$dec2bin -dec $dec }
            'dh' { $dec = $data; &$dec2hex -dec $dec }
            'di' { $dec = $data; &$dec2ip -dec $dec }
            'hb' { $hex = $data; &$hex2bin -hex $hex }
            'hd' { $hex = $data; &$hex2dec -hex $hex }
            'hi' { $hex = $data; &$hex2ip -hex $hex }
            'ib' { $ip = $data; &$ip2bin -ip $ip }
            'id' { $ip = $data; &$ip2dec -ip $ip }
            'ih' { $ip = $data; &$ip2hex -ip $ip }
            default {write-error -message "InvalidOperation: The operation specified is undefined, process will now stop." -Category InvalidOperation}
        }
    }
}
$data = $Args[0]
$task = $Args[1]
NumConversion $data $task
\$\endgroup\$
3
  • 2
    \$\begingroup\$ you state that you want to avoid all the optimized methods available in PoSh and in dotnet ... and then ask for optimization help. that does not make sense to me. [grin] \$\endgroup\$ – Lee_Dailey May 11 at 6:11
  • \$\begingroup\$ An intriguing project, but… A fully functional Posh script? Try NumConversion 1 hd or NumConversion 127.0.0.1 ih. And compare edge examples (here I agree that 1 isn't an IP-like string…) NumConversion 1 ih versus NumConversion 1 id. \$\endgroup\$ – JosefZ May 15 at 19:39
  • \$\begingroup\$ To elaborate the edge case: NumConversion (NumConversion 1 ih) hi returns 0.0.0.1 while NumConversion (NumConversion 1 id) di yields 1.0.0.0. It's an inconsistency, isn't it? \$\endgroup\$ – JosefZ May 15 at 20:04
1
\$\begingroup\$

I don't have enough score to comment, so i'll write an answer instead:

Its a interesting idea. I'm suprised there isn't any command that does this already?

  1. Function should always be verb-job. To get a list of approved verbs, do get-verb in the console. Might i suggest something hing like Invoke-BitConvert?
  2. when asking for one of several 'static' values please use [Validateset('possible,'values')] on the parameter
    1. on the notion of the task argument, it would be more benefitial to the user if you used the full conversion names (IE binaryToDecimal instead of bd). You could still add in the shortnames aswell, but if you read a script that has this its much more clear what the intention of the parameter is
  3. Add [CmdletBinding()] above the param() block to get support for -verbose, -debug, -erroraction etc.
  4. Try to figure out if you can used some dotnet features to help you along with the conversion. they are faster and most likely better tested:
    1. Convert from binary -> dec -> hex: [Convert]::ToInt32($bin,2).tostring("X")
    2. IP check:[ipaddress]::TryParse($ip,[ref]$null) this would return a boolean, making you get rid of that regex.
    3. [system.convert] have many features that convert to and from different types. but they convert ususally to byte array or from byte array
  5. I would advice you to find a intermidiate step between all of these values to convert to and from (mabye decimal?). This would do 2 things:
    1. For each conversion you only need to define how to convert your current "startvalue" to the intermidiate value and how to convert it back.
    2. Any future conversion you choose to add, would also automatically be supported for all current conversions aswell.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.