round() can specify the operating mode when edge case values such as HALF_UP and HALF_DOWN are passed. For example, 0.285 is internally 0.28499999999999998. Should this be considered an edge case? Or is it not an edge case? Regarding this issue, in 2008, the following RFC adopted the policy that 0.285 is considered an edge case. https://wiki.php.net/rfc/rounding
However, there is an opinion that it is wrong to expect decimal-like behavior because FP is just FP. Since 0.28499999999999995 to 0.28500000000000000 are all the same IEEE754 internal representation, and most of their values are less than 0.285, it is unreasonable to treat them as edge cases.
Here are excerpts from the RFC that determined the current implementation policy that are particularly relevant in this context.
If the requested number of places to round the number is smaller than the precision of the number, then the number will be first rounded to its own precision and then rounded to the requested number of places.
Example: Round 1.255 to 2 places precision, expected value is 1.26. First step: Calculate 10^places = 10^2 = 100. Second step: Calculate 14 - floor(log10(value)) = 14 - 0 = 14 which indicates the number of places after the decimal point which are guaranteed to be exact by IEEE 754. Now, 2 < 14, so the condition applies. So, calculate 10^14 and multiply the number by that: 1.255 * 1e14 = 125499999999999.984375... Now, round that number to integer, i.e. 125500000000000. Now, divide that number by 10^(14 - 2) = 10^12 (the difference) and get 125.5 (exact). NOW round that number to decimal which yields 126 and divide it by 10^2 = 100 which gives 1.26 which is the expected result for that rounding operation.
Of course, one may argue that pre-rounding is not necessary and that this is simply the problem with FP arithmetics. This is true on the one hand, but the introduction of the places parameter made it clear that round() is to operate as if the numbers were stored as decimals. We can't revert that and this seems to me to be the best solutions for FP numbers one can get.
In other words, due to the presence of the second parameter, the user expects round() to behave as if it were dealing with a decimal number.
By the way, the double rounding method has side effects and can lead to incorrect results. It will need to be fixed regardless of the outcome of this RFC. I have already created a PR to correct the issue in accordance with PHP current policy. If this RFC is rejected, this PR will be adopted. https://github.com/php/php-src/pull/12268
We can discuss this RFC without getting caught up in the technical issues of whether it is feasible.
FP is not a decimal number. When we treat FP “like” a decimal number, it is not an exact value, but an approximation with some error range. The following six numbers are all 3fd23d70a3d70a3d in IEEE754 representation.
0.28499999999999995 0.28499999999999996 0.28499999999999997 0.28499999999999998 0.28499999999999999 0.28500000000000000
Since this range does contain 0.28500000000000000, we could consider it to be 0.28500000000000000, but that would ignore the fact that the remaining five values are less than 0.285.
And for users who want to round 0.28499999999999998 correctly, such behavior is a bug.
Now look at the following code.
var_dump(0.1 + 0.2 === 0.3);
You'll soon see that this becomes false. However, treating FP as a decimal number means that the above code must be true. I think you can understand that is strange. There is no reason why only round() should behave as a decimal.
Even if we treat FP like a decimal number, we should use the value that is closest to it, and not a value that is “just within the range” that is on the boundary with the next value.
And if we expect FP to behave like a decimal number in round(), we should expect similar behavior for all FPs in PHP. We need to avoid inconsistent implementations where only certain features behave exceptionally.
See warnings on this page. https://www.php.net/manual/en/language.types.float.php
It can be seen that PHP's policy is not to require FP to behave as a decimal number. And as it says in the warning, if we want to calculate as a decimal number, we should use BCMath.
Similar to PHP, we investigated languages that allow you to specify the precision you want to round.
Ruby:
p 0.285.round(2) // 0.29 p 0.28499999999999998.round(2) // 0.29
Python:
// Python rounds to even numbers, so 0.285 cannot be compared. Instead, I use 1.555, which is also 1.5549... due to the FP error. print(round(1.555, 2)) // 1.55 print(round(1.5549999999999999, 2)) // 1.55
Ruby behaves the same as current PHP, and Python behaves the same as this RFC aims to do.
When it comes to databases, there are two types of values: exact values and approximate values. Verify using approximate values to align with PHP. Since even rounding may occur, I use 1.555, just like in Python.
MySQL:
mysql> SELECT 1555E-3 = 15549999999999999E-16; +---------------------------------+ | 1555E-3 = 15549999999999999E-16 | +---------------------------------+ | 1 | +---------------------------------+ mysql> SELECT ROUND(1555E-3, 2); +-------------------+ | ROUND(1555E-3, 2) | +-------------------+ | 1.56 | +-------------------+ mysql> SELECT ROUND(15549999999999999E-16, 2); +---------------------------------+ | ROUND(15549999999999999E-16, 2) | +---------------------------------+ | 1.56 | +---------------------------------+
SQLite3:
sqlite> SELECT 1555E-3 = 15549999999999999E-16; 1 sqlite> SELECT ROUND(1555E-3, 2); 1.56 sqlite> SELECT ROUND(15549999999999999E-16, 2); 1.56
Firebird:
SQL> SELECT 1555E-3 FROM RDB$DATABASE; CONSTANT ======================= 1.554999999999999937828 SQL> SELECT ROUND(1555E-3, 2) FROM RDB$DATABASE; ROUND ======================= 1.560000000000000053291
SqlServer:
1> SELECT 1555E-3; 2> go ------------------------ 1.5549999999999999 1> SELECT ROUND(1555E-3, 2); 2> go ------------------------ 1.55
MySQL, SQLite, Firebird are similar to current PHP. SqlServer is similar to what this RFC is aiming for.
PostgreSQL cannot round double precision values if we specify digits, so omit it.
This RFC proposes changes that will cause round() to stop expecting decimal behavior to FP and start expecting FP to behave as FP.
// previous behavior var_dump(round(0.285, 2)); // float(0.29) var_dump(round(0.28499999999999998, 2)); // float(0.29) // new behavior var_dump(round(0.285, 2)); // float(0.28) var_dump(round(0.28499999999999998, 2)); // float(0.28)
Regarding edge case determination for round(), some values will no longer be determined as edge cases.
next PHP 8.x (Currently the target is 8.4)
None.
Only “standard” is affected.
No impact.
None.
None.
None.
There is no effect on anything other than “standard”.
None.
As per the voting RFC a yes/no vote with a 2/3 majority is needed for this proposal to be accepted. Voting started on 2023-11-24 and will end on 2023-12-08 00:00 GMT.
https://wiki.php.net/rfc/rounding https://github.com/php/php-src/pull/12268 https://externals.io/message/121297
None.