PHP rounding error

14.8k views Asked by At

I'm using PHP 5.2.13 on my linux server. I'm getting weird error when rounding numbers. This is my test case:

<?php
echo "        " . round(1.505, 2) . "\n";
echo "       " . round(11.505, 2) . "\n";
echo "      " . round(111.505, 2) . "\n";
echo "     " . round(1111.505, 2) . "\n";
echo "    " . round(11111.505, 2) . "\n";
echo "   " . round(111111.505, 2) . "\n";
echo "  " . round(1111111.505, 2) . "\n";
echo " " . round(11111111.505, 2) . "\n";
echo "" . round(111111111.505, 2) . "\n";

This is results:

        1.51
       11.51
      111.51
     1111.51
    11111.51
   111111.51
  1111111.5
 11111111.51
111111111.51

Anyone knows what causes this? I can't update PHP, since it's shared server.

6

There are 6 answers

2
Kibbee On BEST ANSWER

This is because the number 1111111.505 can't be represented exactly in floating point notation. The closest it can get is 1111111.5049999999. So what ends up happening is that it converts the number in your code to 1111111.50499999999 and then it does the rounding. Which results in 1111111.5. Floating point numbers have problems in that they can't represent a lot of even seemingly simple decimal numbers with complete accuracy. For instance. the number 0.1 cannot be accurately represented using binary floating numbers. Using Python, if you type in 0.1, it returns 0.10000000000001. Plus or minus a few zeros. This is the reason that some languages such as .Net provide a "Decimal" data type which is able to represent all decimal values within a certain range and number of decimal places. The downside of the decimal data type is that it is slower, and each number takes more space to store.

0
symcbean On

You need tp upgrade the wetwaer - not the PHP version.

echo " " . round(1.505, 2) . "\n";

You seem to think you're asking PHP to return the rounded value of 1.505 - but its actually got to return the decimal version of the rounded version of the binary version of 1.505

Try entering the numbers here and have a look at the binary representations.

1
Shabbyrobe On

Seems to me like you're running into an accuracy problem caused by using floats. Floats are not guaranteed to be completely accurate and you may find that this problem manifests on one system but not on another.

If you really care about precision with arbitrary floating-point numbers, use bcmath or gmp if one is available, though if using bcmath, you'll need to make a bcround() function. The only one I've found which actually works is posted in the comments of the php.net bcscale page:

function bcround($number, $scale=0) {
        $fix = "5";
        for ($i=0;$i<$scale;$i++) $fix="0$fix";
        $number = bcadd($number, "0.$fix", $scale+1);
        return    bcdiv($number, "1.0",    $scale);
}
5
Selay On

If still somebody reach this page with similar problems where floating number subtraction causes error or strange values. I want to explain this problem with a bit more details. The culprit is the floating point numbers. To illustrate the problem, I will explain why with a simple example and you can assume from there why it affecting rounding etc.

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 this problem in floating number calculation in a MIM-104 Patriot missile battery prevented it intercepting an incoming Scud missile in Dhahran, Saudi Arabia, contributing to the death of 28 soldiers 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. 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, uou 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, 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. References: 1,2,3,4,5

0
Andreas Bogavčić On

The code in the answer of Shabbyrobe does not handle negative numbers. This is a working improvement of the code, which handles negative numbers as well as the default scale:

function bcround($number, $scale = null) {
    if (is_null($scale)) {
        $scale = bcscale();
    }
    $fix = ($number < 0) ? '-' : '';
    $fix .= '0.' . str_repeat('0', $scale) . '5';
    $number = bcadd($number, $fix, $scale + 1);
    return bcdiv($number, "1.0", $scale);
}
0
Saint On

@Shabbyrobe: your function is wrong: try to round this number with a scale of 2: 44069.3445

it should be 44069.35, but default php round()- and your Function returns 44069.34

Working code by a member of php.net:

function mround($number, $precision=0) {

$precision = ($precision == 0 ? 1 : $precision);   
$pow = pow(10, $precision);

$ceil = ceil($number * $pow)/$pow;
$floor = floor($number * $pow)/$pow;

$pow = pow(10, $precision+1);

$diffCeil     = $pow*($ceil-$number);
$diffFloor     = $pow*($number-$floor)+($number < 0 ? -1 : 1);

if($diffCeil >= $diffFloor) return $floor;
else return $ceil;
}