Calendar

November 2009
S M T W T F S
    Jan »
1234567
891011121314
15161718192021
22232425262728
2930  

Exhaustion Counter

Tags

The Trouble With Floats, and not the Root Beer Kind

This post is about float values, some basic computer science, and the PHP-specific handling of floats. It’s geared for the beginner, but please read on even if you feel you’re above it, as I tried to include some interesting bits at the end.

As a programmer, I have read through more fat books than I care to think about. That doesn’t mean I’ve retained it – quite the opposite. I’ve probably forgotten at least ten facts for any one I remember. That said, there are times when I’m working on something that I have that “Oh yeahhhh… a-Dur! *self thwap*” moment when something I should know pops up and says hello. Recently, I had a moment like that when I was (foolishly) trying to check a float value for equality in PHP.

Allow me to back the truck up for a moment. In case you don’t know, float values are numbers with a decimal component, e.g. 1.234 . In PHP, you end up with a float any time you explicitly create a decimal, like so:

$x = 2.234; Or, for example, if you divide two integers, like this:

$x = 2 / 3;

You can end up with a float value many other ways, but you get the idea. The trouble with floats is this: the value of a float is always an estimate!. That might not seem like a surprise to you when considering the decimal representation of, say, 2/3. However, it can be annoying if you (as I did) stop thinking of floats as what they are for a moment in the context of what you’re doing.

Before I babble any further, here’s a stripped down code example, illustrating the basic problem I encountered:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
 
$x = .6 + .3 + .1;
var_dump($x); // value of $x shows as "float(1)"
 
if($x == 1) {
    echo "$x = 1. All is right in the world\n";
} else {
    echo "$x != 1? The hell you say?!?\n";
}
 
?>
At this point, beginners might be wondering what’s going on, while experienced programmers might wonder what I was thinking. I’ll explain the scenario in a bit, but the short version is that I was working with percentage values that I expected, under normal conditions, to add up to 100%. But first, let’s clear up the apparent weirdness in the script above. Remember when I said the float values are estimates? That’s what’s going on here. Even though to us, .6 + .3 + .1 is clearly 1, the computer has to do some rounding.

Computers don’t store numbers using base 10. That is, a computer doesn’t think of .6 as six tenths. It tries to store that fraction as best it can using what essentially boils down to ones and zeroes. In that conversion, rounding occurs. It is similar to when we have to represent a fraction like 2/3 as a decimal. Without “cheating” and putting a line over the repeating digit, the best we can do is something like 0.66666667, where the final 7 is the point at which we got tired of writing sixes, gave up, and wrote a 7. We round up, figure that’s close enough, and move on with our lives. The computer does the same thing, but some times it’s not as obvious.

In the above script, when the value of $x is dumped to the screen, it is shown as “float(1)”. That really only adds to the confusion, because it’s clearly not equal to 1. So what is $x? The var_dump function is actually rounding off the value. However, in PHP, you can output the value of a float with forced precision using the printf function, using some formatting directives. Here’s an example: printf("%.30f", $x); The string “%.30f” is what’s called a *conversion specifier* that tells the computer to represent the argument (the $x) as a float with a precision of 30 decimal places. See the documentation for more information. When we add that to the example script and run it, things become a little easier to understand:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
 
$x = .6 + .3 + .1;
echo "var_dump of \$x: \n";
var_dump($x); // value of $x shows as "float(1)"
echo "\n more precise version of \$x:";
printf("%.30f\n\n", $x); // value of $x shows as 0.999999999999999822364316059975
 
if($x == 1) {
    echo "$x = 1. All is right in the world\n";
} else {
    echo "$x != 1? The hell you say?!?\n";
}
 
?>
Ahh – there it is. The value of $x, though close to 1, is really 0.999999999999999822364316059975… as far as the computer is concerned. So even though 60% + 30% + 10% should add up to 100% in our book, the computer may not agree. The question is, how do we make the computer agree? The answer: the same way we got into this mess in the first place – by rounding. PHP’s round() function allows a second optional parameter for precision, indication the number of zeroes after the decimal that we actually care about. In this example, 5 ought to be sufficient. What we can do is round the value we’re testing for a certain precision before the comparison. We’re effectively saying, “Is this number near enough to one as makes no difference?”.

Here’s the final revised code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
 
$x = .6 + .3 + .1;
echo "var_dump of \$x: \n";
var_dump($x); // value of $x shows as "float(1)"
echo "\n more precise version of \$x:\n";
printf("%.30f\n\n", $x); // value of $x shows as 0.999999999999999822364316059975
 
if(round($x, 5) == 1) {
    echo "$x = 1. All is right in the world\n";
} else {
    echo "$x != 1? The hell you say?!?\n";
}
 
?>
Finally, 1 is equal to 1 again, and all is right in the world. We can breathe a sigh of relief, and get back to more important things, like listening to New Model Army.

That does it for the basic bit, and ends the “beginner” part of this post. For the truly masochistic: Here’s a description of the scenario:

I was working on something that involved percentage weights for a list of values. Basically, each value had a weight reflecting its relevance that was factored in when all of the values were added. The sum of the weights was 100%, and were 60%, 30%, and 10% for the 3 values. In essence, here’s how it played out:

$adjusted_value = $value1 * .6 + $value2 * .3 + $value3 * .1;

So far so good. However, I also needed to account for the possibility that all three values might not be present. For the requirements of the problem (and I’m keeping the example much more simplified than the actual code involved), I needed to ’scale’ the adjusted_value by the total weights of the values that were present. For example, if only the first two values were present, the total weight would be 60% + 30% = 90%, so I would have to divide my adjusted_value by the total weight, or:

$total_weight = .6 + .3; $adjusted_value = $adjusted_value / $total_weight;

In the code, I had a check for each value being included, and if it existed (was not null), added the weight to the $total_weight. At this point, everything was groovy. The problem I encountered was when I was checking the value of the $total_weight, and firing code based on whether or not it totaled 100%. What I wanted the code to do at that point is immaterial to the bug, what’s important is that the code was never being fired. Here’s a stripped down version of the code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
$weights = array(.6, .3, .1);
$values = array(75, 62, 80);
 
$count = 0;
$total_weight = 0;
$adjusted_value = 0;
foreach ($values as $v) {
  if (isset($v)) {
    $adjusted_value += $v * $weights[$count];
    $total_weight += $weights[$count];
  }
  $count++;
}
if ($total_weight == 0) { // covers case to avoid divide by zero
  $adjusted_value = 0;
} elseif($total_weight != 1) { // error: this *always* gets fired!
  $adjusted_value /= $total_weight;
} else {
  // code that never gets fired, even at 100%
}
?>
The problem line, as should be obvious is this: } elseif($total_weight != 1) {

Changing the comparison to round the float value being compared gives me the result I expected: } elseif(round($total_weight, 5) != 1) { Once again, all is right in the world.

You must be logged in to post a comment.