27
votes

I have a very strange issue. If I subtract 2 float vars where one is the result of a mathematical operation I get a wrong value.

Example:

var_dump($remaining);
var_dump($this->hours_sub['personal']);
echo $remaining-$this->hours_sub['personal'];

This it the output:

float 5.4
float 1.4
5.3290705182008E-15

5.4-1.4 should be 4 If I add the two values the result is correct.

Where is my mistake? It can not be a rounding issue.

4
It is working fine, maybe you should try bcsub() - HamZa
Why "It can not be a rounding issue."? - Patricia Shanahan
@PatriciaShanahan Because a rounding issue would not produce a value close to 0 for the difference of two approximations to 5.4 and 1.4 respectively. A "rounding issue" would produce 3.999...9xyz or 4.000...0xyz. - Daniel Fischer
Ia am sorry for the question. It was stupid. For everyone who has the same problem. You must cast floatvals to the to the same format using round() or number_format(). See php.net/manual/de/language.types.float.php - RubbelDeCatc

4 Answers

95
votes

If still somebody hits this page with similar problems where floating number subtraction causes error or strange values. Below I will explain this problem with a bit more details.

It is not directly related to PHP and it is not a bug. However, every programmer should be aware of this issue.

This problem even took many lives two decades ago.

On 25 February 1991 an incorrect floating-point arithmetic (called rounding error) in a MIM-104 Patriot missile battery prevented it from intercepting an incoming Scud missile in Dhahran, Saudi Arabia, killing 28 soldiers and injuring near 100 servicemen from the U.S. Army's 14th Quartermaster Detachment.

But why it happens?

The reason is that floating point values represent a limited precision. So, a value might not have the same string representation after any processing (chopped off). It also includes writing a floating point value in your script and directly printing it without any mathematical operations.

Just a simple example:

$a = '36';
$b = '-35.99';
echo ($a + $b);

You would expect it to print 0.01, right? But it will print a very strange answer like 0.009999999999998

Like other numbers, floating point numbers double or float is stored in memory as a string of 0's and 1's. How floating point differs from integer is in how we interpret the 0's and 1's when we want to look at them. There are many standards how they are stored.

Floating-point numbers are typically packed into a computer datum as the sign bit, the exponent field, and the significand or mantissa, from left to right....

Decimal numbers are not well represented in binary due to lack of enough space. So, you can't express 1/3 exactly as it's 0.3333333..., right? Why we can't represent 0.01 as a binary float number is for the same reason. 1/100 is 0.00000010100011110101110000..... with a repeating 10100011110101110000.

If 0.01 is kept in simplified and system-truncated form of 01000111101011100001010 in binary, when it is translated back to decimal, it would be read like 0.0099999.... depending on system (64bit computers will give you much better precision than 32-bits). Operating system decides in this case whether to print it as it sees or how to make it in more human-readable way. So, it is machine-dependent how they want to represent it. But it can be protected in language level with different methods.

If you format the result using

echo number_format(0.009999999999998, 2);

it will print 0.01.

It is because in this case you instruct how it should be read and how precision you require.

Note number_format() is not the only function, a few other functions and ways can be used to tell the programming language about the precision expectation.

References:
https://sdqweb.ipd.kit.edu/publications/pdfs/saglam2016a.pdf
https://en.wikipedia.org/wiki/Round-off_error

4
votes

This worked for me:

<?php
    $a = 96.35;
    $b = 96.01;

    $c = ( ( floor($a * 100) - floor($b * 100) ) / 100 );

    echo $c; // should see 0.34 exactly instead of 0.33999999999999
?>

Since the problem occurs with floating point subtraction operation I decided to eliminate that by transforming it into an integer operation, then backing up the result into a floating point again.

I much prefer that solution because basically it does prevent the error on calculation rather than rouding up the result with other functions.

3
votes

In addition to using number_format(), there are three other ways to obtain the correct result. One involves doing a little math, as follows:

<?php

$a = '36';
$b = '-35.99';

$a *= 100;
$b *= 100;

echo (($a + $b)/100),"\n";

See demo

Or, you could simply use printf():

<?php

$a = '36';
$b = '-35.99';

printf("\n%.2f",($a+$b));

See demo

Note, without the precision specifier, the printf() result will contain trailing zero decimals, as follows: 0.010000

You also could also utilize the BC Math function bcadd(), as follows:

<?php
$a = '36';
$b = '-35.99';
echo "\n",bcadd($a,$b,2);

See demo

0
votes

I wrote a simple function to deal with this. It works similarly to the bcadd function from the bcmath extension of php. You pass it 2 decimal numbers in string form, $a and $b, and specify how many decimals should be used which must match the number of decimals in both $a and $b.

As you can see it will use integers to do the math, then convert back to string without using floating point operations at any point.

function decimalAdd($a,$b,$numDecimals=2) {
    $intSum=(int)str_replace(".","",$a)+(int)str_replace(".","",$b);
    $paddedIntSum=str_pad(abs($intSum),$numDecimals,0,STR_PAD_LEFT);
    $result=($intSum<0?"-":"").($intSum<100&&$intSum>-100?"0":"").substr_replace($paddedIntSum,".",-$numDecimals,0);
    return $result;
}

Sample usage:

echo decimalAdd("36.00","-35.99");

0.01