4

I'm having struggle trying to figure out why some times the substraction of two rounded values gives me a non-rounded value. Is like if 2.2 - 1.1 = 1.10000001.

I have this block of a script in PowerShell:

foreach($disk in $disks) 
{
    $size = $disk.Size
    $freespace = $disk.FreeSpace
    $percentFree = [Math]::Round(($freespace / $size) * 100)
    $sizeGB = [Math]::Round($size / 1073741824, 2)
    $freeSpaceGB = [Math]::Round($freespace / 1073741824, 2)
    $usedSpaceGB = $sizeGB - $freeSpaceGB

    #view the variable values in every iteration
    Write-Debug "`$size = $size"
    Write-Debug "`$freespace = $freespace"
    Write-Debug "`$percentFree = $percentFree%"
    Write-Debug "`$sizeGB = $sizeGB"
    Write-Debug "`$freeSpaceGB = $freeSpaceGB"
    Write-Debug "`$usedSpaceGB = $usedSpaceGB"
}

The troubling part is:

$usedSpaceGB = $sizeGB - $freeSpaceGB

The [Math]::Round() method seems to work as intended, because I can see the rounded values stored in $sizeGB and $freeSpaceGB variables. Here are some debug output examples:

debug output 1 debug output 2

  1. I don't understand why in some cases the subtraction of two previously rounded values results in a non-rounded value as you can see in example images.
  2. This behavior happens in the same exact positions of the loop, lets say in the 7, 10 and 18 iteration.

I export this data to an HTML file so the table looks odd with this values not rounded:

enter image description here

Right now I "fixed" this by Rounding also the subtraction result but I'm wondering why is this happening. I hope someone can help me to understand what is going on.

smartdan
  • 73
  • 1
  • 6
  • 9
    I assume `$disks` is a collection of [`Win32_LogicalDisk`](https://msdn.microsoft.com/library/aa394173.aspx) instances? If so, `FreeSpace` and `Size` are integer types (both `UInt64`), but by passing them to [`Math.Round`](https://msdn.microsoft.com/library/system.math.round.aspx) you are getting a `Double` back, which is then subject to the usual issue of [floating-point numbers not being able to exactly represent some numbers](https://stackoverflow.com/questions/1089018/why-cant-decimal-numbers-be-represented-exactly-in-binary). I would round only the final output, not intermediate values. – Lance U. Matthews Sep 18 '17 at 19:46

1 Answers1

4

Alright, let's try this as an answer...

I am assuming $disks is a collection of Win32_LogicalDisk instances, in which case the FreeSpace and Size properties are integer types (specifically, UInt64). If $disks contains some other type(s), those two properties are likely integers of some kind since we're dealing with byte counts.

This is somewhat extraneous to the question, but in this line...

$percentFree = [Math]::Round(($freespace / $size) * 100)

...$percentFree will contain a Double even though you're calling the Math.Round overload that rounds to the nearest integer because that's the return type of that method. You can inspect this by evaluating...

$percentFree.GetType()

If you want/expect $percentFree to contain an integer type then you need to cast it to one, like this...

$percentFree = [UInt64] [Math]::Round(($freespace / $size) * 100)

The above also applies when you are using Math.Round to round to the nearest hundredth...

$sizeGB = [Math]::Round($size / 1073741824, 2)
$freeSpaceGB = [Math]::Round($freespace / 1073741824, 2)

...because, of course, that method overload has to return a floating-point type since, by definition, an integer type could not store fractions of a value. Thus, $sizeGB and $freeSpaceGB contain floating-point values (specifically Double) so when this line...

$usedSpaceGB = $sizeGB - $freeSpaceGB

...is executed $usedSpaceGB will also contain a Double, in which case all bets are off as far as it being able to exactly represent the resulting value.

Hopefully that explains why this is happening. As far as how to improve your code, first I would recommend not doing any rounding on intermediate values...

$sizeGB = $size / 1073741824
$freeSpaceGB = $freespace / 1073741824
$usedSpaceGB = [Math]::Round($sizeGB - $freeSpaceGB, 2)

...which can be written more clearly and concisely as...

$sizeGB = $size / 1GB
$freeSpaceGB = $freespace / 1GB
$usedSpaceGB = [Math]::Round($sizeGB - $freeSpaceGB, 2)

This won't eliminate floating-point approximations like you're seeing, but $usedSpaceGB will be closer to the actual value since you're not computing based on the already-rounded (which discards some information) values of $sizeGB and $freeSpaceGB.

In the previous snippets, $usedSpaceGB is still a Double so it's still possible it computes to some value that can't be represented exactly. Since this value is being formatted for display or otherwise must be serialized to a string (as HTML) I would just forget about Math.Round and let string formatting handle the rounding for you like this...

$usedSpaceGBText = (($size - $freeSpace) / 1GB).ToString('N2')

...or this...

$usedSpaceGBText = '{0:N2}' -f (($size - $freeSpace) / 1GB)
Lance U. Matthews
  • 15,725
  • 6
  • 48
  • 68