Translation(s): none


Why Rounding Does Not Always Work As Expected

People sometimes notice that rounding isn't always humanly intuitive or correct. A problem with rounding in PHP was found and reported in bug #603292 that illustrates the problem. This page captures the discussion there so that it will remain available after the bug is archived.

The Rounding Problem

Example code:

<?php
echo round(1.025, 2) . "<br />\n";
echo round(1.125, 2) . "<br />\n";
echo round(1.225, 2) . "<br />\n";
?>

In Lenny PHP 5.2.6 this prints:

1.02
1.13
1.23

But That Is The Way It Works

Obviously that isn't right! Is it? What is going on? This is exactly the same behavior as the C library printf. It is exactly the same as what is exposed in the shell printf command. You can try this yourself from the command line. Here is an example from the shell's command line printf program that you can run from the command line.

$ printf "%.2f\n" 1.025
1.02

PHP is behaving the same as the shell's printf and the same as the libc printf. Obviously this can't be a bug then because all of these are behaving exactly the same way. But it just didn't seem satisfying to say that it isn't a bug in PHP because that is just the way floating point computer engines work. But that is basically the reason. That is the way floating point works on machines. But being an unsatisfying answer this is an attempt to add more information about the problem. It isn't correct. But neither is it a correctable bug.

Why It Does Not Behave As We Expect

For the underlying reasons you need to look at the encoded binary values. A typical issue is that exact terminating decimal fractions may not have an exact binary representation. Therefore converting from decimal to binary introduces an error in representation that did not exist in the decimal form. Look at these examples.

1.020 => 1.00000101000111101011100001010001111010111000010100011110101110...
1.025 => 1.00000110011001100110011001100110011001100110011001100110011001...
1.030 => 1.00000111101011100001010001111010111000010100011110101110000101...

Since there are an infinite number of digits needed to represent those values they can't be used without reducing the number of digits in the calculation. That means that floating point operations routinely introduce errors into calculations.

Authors Note: As to the exact details of rounding fractional digits I have always avoided needing to know that information. I have avoided it because when I needed an exact value I knew about the problems and instead used scaled integers. And so I can't give a better explanation without researching the problem in more detail. I can only give a recommendation to avoid it too. If someone could find a good reference to link to on numerical analysis and link it in here that would be great. I looked at the Wikipedia page but at the time of writing this didn't find it worthy enough to included here. Perhaps improving that page first would be the best case help.

Floating Point Is Only An Approximation

Remember that floating point is only an approximation of reality. For more exact answers you may want to use binary coded decimal or scaled integers. This example is a good one to show why floating point isn't used for monetary transactions. The accounting can never be exact. Therefore instead of fractional dollars a whole number of pennies are counted. Pennies are scaled to dollars with a multiplication factor of 100. CAD/EDA software is another example that often uses scaled integers to avoid these types of error. A VLSI layout may use a grid of 0.01 microns. An integer number of grid points are measured and the result scaled by hundred million. By working with integers the errors introduced by floating point are avoided.

Recent Changes in PHP

There have been recent changes in the way PHP handles rounding. The upstream PHP documentation lists:

In 5.3.0 and later (Sid or Squeeze) the mode parameter gives you some control over rounding. Lenny includes PHP version 5.2.6. Squeeze includes PHP version 5.3.3. The mode parameter may be one of PHP_ROUND_HALF_UP, PHP_ROUND_HALF_DOWN, PHP_ROUND_HALF_EVEN, or PHP_ROUND_HALF_ODD. Even rounding would be a typical scientific rounding.

$ php -r 'echo round(1.025,2,PHP_ROUND_HALF_UP) . "\n";'
1.03
$ php -r 'echo round(1.025,2,PHP_ROUND_HALF_DOWN) . "\n";'
1.02
$ php -r 'echo round(1.025,2,PHP_ROUND_HALF_EVEN) . "\n";'
1.02
$ php -r 'echo round(1.025,2,PHP_ROUND_HALF_ODD) . "\n";'
1.03

Also the default rounding mode has changed too.

$ php -r 'echo round(1.025,2) . "\n";'
1.03

Here are some useful references.