ID:               42294
 Comment by:       chris_se at gmx dot net
 Reported By:      oliver at teqneers dot de
 Status:           Assigned
 Bug Type:         *Configuration Issues
 Operating System: OpenSuSE 10.2
 PHP Version:      5.2.4RC1
 Assigned To:      iliaa
 New Comment:

I just read this bug report and wanted to add a few things. Rounding
floating point numbers is anything but trivial.

The core issue is that certain numbers which are representable with
only a finite amount of digits in the decimal system are not necessarily
representable with a finite amount of digits in the binary system. The
number 0.285 in this bug report is an example for that: In the binary
system, its representation is periodic - just as 1/3 can only be
displayed as a periodic number (0.333333333...) in the decimal system.

Since a floating point number only supports a finite number of digits,
the period is "cut off" and therefore the number 0.285 stored as a float
is not exactly 0.285 but slightly smaller, you can try it yourself:
2
<?php
$f = 0.285;
printf("%.20f\n", $f);
?>
0.28499999999999997558

(The exact representation may vary depending on how percise the
floating point unit in your processor is.)

Another number 1.285 mentioned in this thread also has the same
problem:

<?php
$f = 1.285;
printf("%.20f\n", $f);
?>
1.28499999999999992006

Now, the traditional rounding method in the decimal system is to take
the lower number for the digits 0, 1, 2, 3 and 4 and the higher number
for the digits 5, 6, 7, 8 and 9. So 1.4 becomes 1 and 1.5 becomes 2 if
rounded to zero digits precision.

The problem is that if the internal representation of the floating
point number is 0.2849...something instead of 0.285, the rounding
algorithm will incorrectly assume the last digit is a 4 and not a 5 and
then return the lower number instead of the higher one.

Now one may ask why does 1.285 work and 0.285 doesn't if both are not
representable using finite digits in the binary system? This is due to
the way the rounding algorithm works: It first multiplies the numbers by
10 to the power of the places of precision (with 2 places precision, it
multiplies them with 100) and then it rounds to the next integer. Now,
if you have a look at the representation of 1.285 * 100 and 0.285 * 100,
you will get:

<?php
$f = 1.285 * 100;
printf("%.20f\n", $f);
$f = 0.285 * 100;
printf("%.20f\n", $f);
?>
128.50000000000000000000
28.49999999999999644729

Of course, one might argue that 28.5 is infact representable as a
floating point number - sure, but that does not matter the computer
always calculates with floating point numbers - so in the case of 128.5
the computer actually makes two errors due to decreased precision: The
first is not being able to correctly represent 1.285 and the second is
to accidentally compensate that error due to lack of precision. With
0.285, only the first error happens and so the result is incorrect.

So that's the reason why round() does not always work as expected. Now
there are two possibilities to solve this:

1) Don't give a shit about the error and simply calculate as before.
This is what the Linux implementation of the C99 function round(3) does
(and probably the C99 standard itself, but I don't know since I haven't
looked into it).

2) Try to correct the error: This is what the PHP_ROUND_FUZZ code is
fore. A bit of background: A round() function is available in C only
from C99 onwards - to ensure compability, PHP does rounding manually
using floor/ceil. In order to keep this post short, I'll just look at
positive numbers. So the current implementation of PHP's round() as
found in ext/standard/math.c does the following:

#define PHP_ROUND_WITH_FUZZ(val, places) {                      \
        double tmp_val=val, f = pow(10.0, (double) places);     \
        tmp_val *= f;                                   \
        if (tmp_val >= 0.0) {                           \
                tmp_val = floor(tmp_val + PHP_ROUND_FUZZ);      \
        } else {                                        \
                tmp_val = ceil(tmp_val - PHP_ROUND_FUZZ);       \
        }                                               \
        tmp_val /= f;                                   \
        val = !zend_isnan(tmp_val) ? tmp_val : val;     \
}                                                       \

Let's assume for a moment that PHP_ROUND_FUZZ is 0.5, then the code is
obvious: 0.5 is added to the number and then floor() is called. That
will produce the identical result for positive numbers as round() does.

Now, a possible correction for the rounding error is setting
PHP_ROUND_FUZZ to 0.50000000001 - the last digit 1 does just enough to
make round() work as expected.

Obviously, this code has one minor drawback: If one wants to round
0.49999999999 to 0 places precision, the "corrected" function will
incorrectly return 1 instead of 0 here. On the other hand, this tiny
fuzz will correct the VAST majority of other cases where round() fails.

So, now - what does PHP do? It has a configure check that tries to
figure out if the fuzz is to be applied - by testing rounding with a
sample number (0.045 to two places). What was the problem of the
original reporter? 0.045 was a number where the "second error correcting
the first one" kicks in if it is rounded on a 64 bit system with a
higher internal precision of the FPU (probably 128 bit instead of 80 or
something like that). His idea was to additionally test another number -
0.285 to two digits. And well, somehow because of the #ifndef in math.c,
it always disables the fuzz for Windows systems.

My problem with this approach PHP currently uses is the following: The
test does not make sense! There are ALWAYS cases where rounding will not
work when using floating point numbers (either with a correction or
without). So - if the test was written correctly - it would ALWAYS fail
- there is NOT THE SLIGHTEST POSSIBILITY that a system implementing
floating point semantics the same way PHP expects it to will always
deliver correct rounding results! And the fuzz would ALWAYS be used! On
the other hand, the fuzz is apparently ALWAYS disabled on Windows
system.

Now, if I want to write a PHP script, I want it to be portable. I don't
want to have to think about how much FPU precision somebody has or if
it's a 32bit or 64bit system or if it's a Windows, Linux or Mac OS X
box. So in my eyes, there are only three sensible ways of solving this
problem:

1) Do it as the Linux C99 implementation does: Don't care about the
   errors and simple use the simple formula
   floor(value * 10^(places) + 0.5) / 10^(places)
   (and for negative values accordingly)
   But then DOCUMENT this behaviour so that anybody who wants to
   use the round function knows exactly what expects him.
2) Always correct the error with the fuzz and don't care about the
   extremely rare cases where the fuzz actually is contraproductive.
   But also then: DOCUMENT this behaviour.
3) Make the behaviour configurable either in the php.ini or with an
   additional optional parameter to round() or something else.

Staying with the current approach (some systems have it activated, some
don't) will always cause portability issues of the round function. Don't
do that. Choose the way you want the function to behave make sure it
does so on EVERY operating system and architecture.


Previous Comments:
------------------------------------------------------------------------

[2007-08-16 11:39:05] [EMAIL PROTECTED]

Assigned to Ilia who implemented that PHP_ROUND_FUZZ.

------------------------------------------------------------------------

[2007-08-16 11:34:25] oliver at teqneers dot de

you might use a windows machine? there the fuzzy seems to be always
turned off (I don't know why though???). 

math.c:
-------
#ifndef PHP_ROUND_FUZZ
# ifndef PHP_WIN32
#  define PHP_ROUND_FUZZ 0.50000000001
# else
#  define PHP_ROUND_FUZZ 0.5
# endif
#endif




But I have to admit, that I forgot to switch to the php5 versions on
our 32bit server. So on all our PHP4 versions with 32bit, round seems to
work as mathematically expected. Sorry for that. 

BUT still the result could be correct with PHP_ROUND_FUZZ on, like it
did with PHP4. I compiled the latest PHP5 (5.2.4RC1) and changed the
configure check to be more precise. After that the round was working
much better, than it did before. My current result with the patched
configure script is:
0.285 - 0.29
1.285 - 1.29 
1.255 - 1.26

I mean, why is the FUZZ implemented and not used???

------------------------------------------------------------------------

[2007-08-16 10:30:57] [EMAIL PROTECTED]

Output for me on a 32bit system:
0.285 - 0.28
1.285 - 1.29
1.255 - 1.25


------------------------------------------------------------------------

[2007-08-16 10:29:52] [EMAIL PROTECTED]

Those results you get are NOT incorrect. If you round using 2 decimals,
of course that's what you get.

------------------------------------------------------------------------

[2007-08-15 11:25:09] oliver at teqneers dot de

this is an example of the comment block of the round documentation
(http://de.php.net/manual/en/function.round.php):

<?php

printf("0.285 - %s <br> ",round(0.285,2));      
// incorrect result 0.28

printf("1.285 - %s <br> ",round(1.285,2));      
// correct result 1.29

printf("1.255 - %s <br><br>",round(1.255,2));   
// incorrect result 1.25

?>

------------------------------------------------------------------------

The remainder of the comments for this report are too long. To view
the rest of the comments, please view the bug report online at
    http://bugs.php.net/42294

-- 
Edit this bug report at http://bugs.php.net/?id=42294&edit=1

Reply via email to