While working on a project, I had the need to convert a decimal to a fraction and while I know that I could do some math outside of PowerShell (or look it up online), I wanted to be able to do this in PowerShell. Unfortunately, I couldn’t find anything in the PowerShell world that did this. That being the case, I decided to take this on and see what I could do.
This was a nice challenge coming up with a way to take a decimal and then present it as a fraction (note that the fraction is just going to be a string). In the end, I went with iterating through numbers for both the numerator and the denominator until I either get close to a usable fraction or actually find the fraction itself. This may not be the most efficient way of accomplishing the task, but against smaller decimals it works just fine.
My parameters are:
- Decimal
- The value that we will converting to a fraction
- ClosesDenominator
- Used to get the most precision for the fraction by getting as close to the accurate value as possible
- ShowMixedFraction
- Displays the value as a Mixed Fraction instead of an Improper Fraction
- AsObject
- Displays the value as an object instead of as a fraction
Param( [parameter(ValueFromPipeline=$True,ParameterSetName='Object')] [parameter(ValueFromPipeline=$True,Position=0,ParameterSetName='NonObject')] [double]$Decimal = .5, [parameter(ParameterSetName='Object')] [parameter(ParameterSetName='NonObject')] [int]$ClosestDenominator = 100, [parameter(ParameterSetName='NonObject')] [switch]$ShowMixedFraction, [parameter(ParameterSetName='Object')] [switch]$AsObject )
Up next is the piece of code that I use to set up to begin looking for a fraction:
$Break = $False $Difference = 1 If ($Decimal -match '^(?<WholeNumber>\d*)?(?<DecimalValue>\.\d*)?$') { $WholeNumber = [int]$Matches.WholeNumber $DecimalValue = [double]$Matches.DecimalValue } $MaxDenominatorLength = ([string]$ClosestDenominator).Length $DecimalLength = ([string]$Decimal).Split('.')[1].Length+1 $LengthDiff = $MaxDenominatorLength - $DecimalLength If ($LengthDiff -lt 0) { Write-Warning @" Decimal <$($Decimal)> is greater of length than expected Denomimator <$($ClosestDenominator)>. Increase the size of ClosestDenomimator to <$(([string]$ClosestDenominator).PadRight($DecimalLength,'0'))> to produce more accurate results. "@ }
Here I am making sure that I can break when I need to (but just not yet) by setting $Break to $False. $Difference is used to help determine how close I am getting to finding that accurate fraction. In the case that I have a whole number with a decimal, I need to split those off to handle each one on its own. Lastly, I throw a friendly warning if the decimal length is larger than my ClosestDenominator length as this will affect the accuracy of the fractions. This is a little unique because if you want to deal with a repeating decimal, such as .33333…, then you know that it will be 1/3 but unless the ClosestDecimal is shorter than the decimal, then you will the accurate result of 3333/10000 in this case instead of 1/3. Just something to keep in mind and I will demo this later on.
Now onto the main part of the code that starts the recursive searching for the fraction:
If ($DecimalValue -ne 0) { #Denonimator - Needs to be 2 starting out For ($Denominator = 2; $Denominator -le $ClosestDenominator; $Denominator++) { #Numerator - Needs to be 1 starting out For ($Numerator = 1; $Numerator -lt $Denominator; $Numerator++) { Write-Verbose "Numerator:$($Numerator) Denominator:$($Denominator)" #Try to get as close to 0 as we can get $temp = [math]::Abs(($DecimalValue - ($Numerator / $Denominator))) Write-Verbose "Temp: $($Temp) / Difference: $($Difference)" If ($temp -lt $Difference) { Write-Verbose "Fraction: $($Numerator) / $($Denominator)" $Difference = $temp $Object = [pscustomobject]@{ WholeNumber = $WholeNumber Numerator = $Numerator Denominator = $Denominator } If ($Difference -eq 0) { $Break = $True } } If ($Break) {BREAK} } If ($Break) {BREAK} } } Else { $Object = [pscustomobject]@{ WholeNumber = $WholeNumber Numerator = 0 Denominator = 1 } } If ($Object) { If ($PSBoundParameters.ContainsKey('AsObject')) { $Object } Else { If ($Object.WholeNumber -gt 0) { If ($PSBoundParameters.ContainsKey('ShowMixedFraction')) { "{0} {1}/{2}" -f $Object.WholeNumber, $Object.Numerator, $Object.Denominator } Else { $Numerator = ($Object.Denominator * $Object.WholeNumber)+$Object.Numerator "{0}/{1}" -f $Numerator,$Object.Denominator } } Else { "{0}/{1}" -f $Object.Numerator, $Object.Denominator } } }
A lot of things happening here, but nothing too crazy going on. I start out by setting my starting Denominator to 2 (doing a 1 would treat anything greater than 0 as a whole number) and begin using that in a For() loop. This will continue to increment until I hit the ClosestDenominator that is defined by the parameter. Next, the numerator will begin working to increment by starting at 1 and will increment to the size of the Denominator before resetting for the next Denominator.
Now I take the decimal that I am trying to convert to a fraction and subtract it by the value of the division of the numerator and denominator to get as close or equal to zero. If I can get to zero, then I have the most accurate fraction, otherwise I continue going and go with the last closes value. An example is here showing how we get 3/4 from .75:
From there it is just a matter of taking the object that is created and formatting it to the type of fraction that we are expecting.
With all of that said, it is time for some examples of this in action.
Convert-DecimalToFraction –Decimal .5
Convert-DecimalToFraction –Decimal .33
Convert-DecimalToFraction –Decimal .333
Convert-DecimalToFraction –Decimal 1.15
Convert-DecimalToFraction –Decimal 1.15 –ShowMixedFraction
Convert-DecimalToFraction –Decimal 1.15 –AsObject
Convert-DecimalToFraction –Decimal .005 –ClosestDenominator 1000
Give the function a download from the link below and let me know what you think!
Download Convert-DecimalToFraction
https://gallery.technet.microsoft.com/scriptcenter/Convert-a-Decimal-to-a-7dc416be
I find the -AsObject an interesting switch. In many cases I need the options of producing human-readable report output and the option of producing machine-oriented output. I scanned some other source libraries for any prior-art of a -AsObject switch but did not discover any. Is this your first time to use -AsObject ?
Thanks for the comment! This is actually the second time that I have had to use a -AsObject to meet my design requirements for a function in which the human readable output wouldn’t necessarily fit as an object. The other time I did this was with my Get-Constructor function that I wrote a while back here: https://learn-powershell.net/2013/03/14/get-available-constructors-using-powershell/
I’ve found it rare to have to do this, but it is definitely something that I will come back to if the need requires it.