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'd think, "Hey, that should give me 0.01
, right?" But instead, the computer says, "Nope, it's 0.009999999999998
." Why? It's all down to how the computer deals with numbers.
Like whole numbers (which we call integers), floating point numbers (fractions) double or float is stored in memory as a string of 0's and 1's. How decimals differ from integers is in how we interpret the 0's and 1's when we want to look at them. There are some standards how they are stored (particularly, IEEE 754 standard).
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.
It is similar in other languages. For example, in Python, the decimal module provides arbitrary precision arithmetic. In Java, the BigDecimal class can be used. Besides using these built-in features, you can also use libraries that provide arbitrary precision arithmetic, or adjust your program's logic to minimize the impact of floating point errors.
To recap, floating point errors are a consequence of the limited number of bits available for storing numerical data. It's not solely a problem of representation in binary but the issue also arises from trying to represent an infinite number of real numbers with a finite number of bits.
References:
https://sdqweb.ipd.kit.edu/publications/pdfs/saglam2016a.pdf
https://en.wikipedia.org/wiki/Round-off_error