PHP Logo

A while back we found an odd bug that only occurred with some inputs while doing calculations that involved decimals. When we performed a series of multiplications and additions and then compared our calculation with an expected result (using ==) they didn’t show up as equal.

In this article, we’ll discuss why this is and what we can do to prevent it.

When Are Two Numbers Not Equal?

First, let’s look at what happens when we try to compare the result of an addition to our expected result.

$expected = 12.002;
$calculated = 12.001 + 0.001;

Now we would expect $calculated to be equal to $expected but what happens if we try to actually compare them using the following code.

if ($expected == $calculated) {
    echo "Equal", PHP_EOL;
} else {
    echo "Not Equal", PHP_EOL;
}

The output of this is as follows.

Not Equal

Odd right? So something must have happened in the addition to make our two numbers are different. Let’s perform a little echo debugging.

echo $expected, PHP_EOL;
echo $calculated, PHP_EOL;
12.002
12.002

Also weird…

Let’s look at two slightly different numbers

$expected = 12.0002;
$calculated = 12.0001 + 0.0001;

if ($expected == $calculated) {
    echo "Equal", PHP_EOL;
} else {
    echo "Not Equal", PHP_EOL;
}
Equal

When is Subtraction Wrong?

Surely this is a problem with addition right? Let’s try some subtraction. Let’s say we have an application where we’re subtracting some amount of money (subtrahend) from a balance (minuend). This is an example where we really need to make sure we’re accurate.

$minuend = 12.00;
$subtrahend = 11.99;

echo $minuend - $subtrahend, PHP_EOL;
0.0099999999999998

Well, that’s not great but it’s not exactly what we expected. In the short run, this shouldn’t be a problem but what if we’re doing hundreds or thousands of calculations? This inaccuracy could get us into major trouble.

Why?

When we look at a number like 11.99 we can easily represent that number in our heads. But when PHP gets to 11.99 it has to determine a way to represent that number so PHP can use it later. It could store it as a string but then when we asked PHP to perform some kind of calculation on the number it would be slow. Because of that, PHP stores any number that has a decimal as what’s called a floating-point number.

Floating-point numbers allow us to store really tiny numbers (somewhere around 1x10^-300) to really large numbers (1.8x10^308). But we’re storing that number in 64-bits (for most environments these days) of information so it can’t keep all of the digits.

Computer science is all about trade-offs. So in order to get those 64-bits to hold as large of a number as possible, it’s actually holding two pieces of information. The first is the significand which is the leading part of the number and the second is an exponent. Because computers think in 2’s the significand is actually a series of sums of multiples of 1/2. This allows us to represent a lot of numbers but not every possible number can be represented. This causes there to be some potential data loss when PHP stores the number.

Let’s look at an example.

Let’s say we trying to output the number “1,234,567,890.000001”

We’ll write the following script

$number = 1234567890.000001;
echo $number;

When we run our script we get the following output.

1234567890

In this case when PHP ran $number = 1234567890.000001; it converted the number internally to 1.234567890000001x10^9 but then when it attempted to place this in memory there wasn’t enough “space” to store the 0.000001 so it truncated the number to 1.234567890x10^9.

How Do We Fix This?

Now that we know what’s wrong, how do we fix our code?

Let’s look at how we can check to see if two floating-point numbers are equal. Instead of using the == operator, we’re going to calculate the absolute difference between the two numbers and then see if the difference is less than an allowed difference. This allowed difference is going to be very application dependent but we’ve found that 0.0001 is a good starting point as long as we’re not working with very small numbers in which case it will need to be much smaller.

$expected = 12.002;
$calculation = 12.001 + 0.001;
$allowedDifference = 0.0001;

if (abs($expected - $calculation) < $allowedDifference) {
    echo "Equal", PHP_EOL;
} else {
    echo "Not Equal", PHP_EOL;
}

Now when we run it we get what we expected.

Equal

For cases where we’re outputting a floating-point number, we should be rounding the value before displaying it so we don’t have a huge number of places after the decimal.

 
$minuend = 12.00;
$subtrahend = 11.99;

echo round($minuend - $subtrahend, 2), PHP_EOL;
0.01

Finally, when working with currency the best solution is to either use a library specifically build to handle all these edge cases or just track the values using integers (in USD we would track the number of cents and no dollars).

Conclusion

Hopefully, this article has been helpful to you. If so please share it!