diff --git a/NEWS b/NEWS index 1fd5c2e374d55..ce47d11ef0ab6 100644 --- a/NEWS +++ b/NEWS @@ -10,5 +10,7 @@ DOM: Standard: . Implement GH-12188 (Indication for the int size in phpinfo()). (timwolla) + . Partly fix GH-12143 (Incorrect round() result for 0.49999999999999994). + (timwolla) <<< NOTE: Insert NEWS from last stable release here prior to actual release! >>> diff --git a/UPGRADING b/UPGRADING index 73f50ea5d87d6..1dfb256097d2c 100644 --- a/UPGRADING +++ b/UPGRADING @@ -50,6 +50,15 @@ PHP 8.4 UPGRADE NOTES 5. Changed Functions ======================================== +- Standard: + . The internal implementation for rounding to integers has been rewritten + to be easier to verify for correctness and to be easier to maintain. + Some rounding bugs have been fixed as a result of the rewrite. For + example previously rounding 0.49999999999999994 to the nearest integer + would have resulted in 1.0 instead of the correct result 0.0. Additional + inputs might also be affected and result in different outputs compared to + earlier PHP versions. + ======================================== 6. New Functions ======================================== diff --git a/ext/standard/math.c b/ext/standard/math.c index 74ffa565031a3..05f2252e304e8 100644 --- a/ext/standard/math.c +++ b/ext/standard/math.c @@ -93,27 +93,73 @@ static inline double php_intpow10(int power) { /* {{{ php_round_helper Actually performs the rounding of a value to integer in a certain mode */ static inline double php_round_helper(double value, int mode) { - double tmp_value; + double integral, fractional; + + /* Split the input value into the integral and fractional part. + * + * Both parts will have the same sign as the input value. We take + * the absolute value of the fractional part (which will not result + * in branches in the assembly) to make the following cases simpler. + */ + fractional = fabs(modf(value, &integral)); + + switch (mode) { + case PHP_ROUND_HALF_UP: + if (fractional >= 0.5) { + /* We must increase the magnitude of the integral part + * (rounding up / towards infinity). copysign(1.0, integral) + * will either result in 1.0 or -1.0 depending on the sign + * of the input, thus increasing the magnitude, but without + * generating branches in the assembly. + * + * This pattern is equally used for all the other modes. + */ + return integral + copysign(1.0, integral); + } - if (value >= 0.0) { - tmp_value = floor(value + 0.5); - if ((mode == PHP_ROUND_HALF_DOWN && value == (-0.5 + tmp_value)) || - (mode == PHP_ROUND_HALF_EVEN && value == (0.5 + 2 * floor(tmp_value/2.0))) || - (mode == PHP_ROUND_HALF_ODD && value == (0.5 + 2 * floor(tmp_value/2.0) - 1.0))) - { - tmp_value = tmp_value - 1.0; - } - } else { - tmp_value = ceil(value - 0.5); - if ((mode == PHP_ROUND_HALF_DOWN && value == (0.5 + tmp_value)) || - (mode == PHP_ROUND_HALF_EVEN && value == (-0.5 + 2 * ceil(tmp_value/2.0))) || - (mode == PHP_ROUND_HALF_ODD && value == (-0.5 + 2 * ceil(tmp_value/2.0) + 1.0))) - { - tmp_value = tmp_value + 1.0; - } + return integral; + + case PHP_ROUND_HALF_DOWN: + if (fractional > 0.5) { + return integral + copysign(1.0, integral); + } + + return integral; + + case PHP_ROUND_HALF_EVEN: + if (fractional > 0.5) { + return integral + copysign(1.0, integral); + } + + if (fractional == 0.5) { + bool even = !fmod(integral, 2.0); + + /* If the integral part is not even we can make it even + * by adding one in the direction of the existing sign. + */ + if (!even) { + return integral + copysign(1.0, integral); + } + } + + return integral; + case PHP_ROUND_HALF_ODD: + if (fractional > 0.5) { + return integral + copysign(1.0, integral); + } + + if (fractional == 0.5) { + bool even = !fmod(integral, 2.0); + + if (even) { + return integral + copysign(1.0, integral); + } + } + + return integral; } - return tmp_value; + ZEND_UNREACHABLE(); } /* }}} */ diff --git a/ext/standard/tests/math/round_gh12143_1.phpt b/ext/standard/tests/math/round_gh12143_1.phpt new file mode 100644 index 0000000000000..390852dba5f26 --- /dev/null +++ b/ext/standard/tests/math/round_gh12143_1.phpt @@ -0,0 +1,27 @@ +--TEST-- +GH-12143: Test rounding of 0.49999999999999994. +--FILE-- + %+.17g\n", $mode, $number, round($number, 0, constant($mode))); + } +} +?> +--EXPECT-- +PHP_ROUND_HALF_UP : +0.49999999999999994 -> +0 +PHP_ROUND_HALF_DOWN : +0.49999999999999994 -> +0 +PHP_ROUND_HALF_EVEN : +0.49999999999999994 -> +0 +PHP_ROUND_HALF_ODD : +0.49999999999999994 -> +0 +PHP_ROUND_HALF_UP : -0.49999999999999994 -> -0 +PHP_ROUND_HALF_DOWN : -0.49999999999999994 -> -0 +PHP_ROUND_HALF_EVEN : -0.49999999999999994 -> -0 +PHP_ROUND_HALF_ODD : -0.49999999999999994 -> -0 diff --git a/ext/standard/tests/math/round_gh12143_2.phpt b/ext/standard/tests/math/round_gh12143_2.phpt new file mode 100644 index 0000000000000..dc5ac74259880 --- /dev/null +++ b/ext/standard/tests/math/round_gh12143_2.phpt @@ -0,0 +1,27 @@ +--TEST-- +GH-12143: Test rounding of 0.50000000000000011. +--FILE-- + %+.17g\n", $mode, $number, round($number, 0, constant($mode))); + } +} +?> +--EXPECT-- +PHP_ROUND_HALF_UP : +0.50000000000000011 -> +1 +PHP_ROUND_HALF_DOWN : +0.50000000000000011 -> +1 +PHP_ROUND_HALF_EVEN : +0.50000000000000011 -> +1 +PHP_ROUND_HALF_ODD : +0.50000000000000011 -> +1 +PHP_ROUND_HALF_UP : -0.50000000000000011 -> -1 +PHP_ROUND_HALF_DOWN : -0.50000000000000011 -> -1 +PHP_ROUND_HALF_EVEN : -0.50000000000000011 -> -1 +PHP_ROUND_HALF_ODD : -0.50000000000000011 -> -1 diff --git a/ext/standard/tests/math/round_gh12143_3.phpt b/ext/standard/tests/math/round_gh12143_3.phpt new file mode 100644 index 0000000000000..ee462fdebaef7 --- /dev/null +++ b/ext/standard/tests/math/round_gh12143_3.phpt @@ -0,0 +1,27 @@ +--TEST-- +GH-12143: Test rounding of 0.5. +--FILE-- + %+.17g\n", $mode, $number, round($number, 0, constant($mode))); + } +} +?> +--EXPECT-- +PHP_ROUND_HALF_UP : +0.5 -> +1 +PHP_ROUND_HALF_DOWN : +0.5 -> +0 +PHP_ROUND_HALF_EVEN : +0.5 -> +0 +PHP_ROUND_HALF_ODD : +0.5 -> +1 +PHP_ROUND_HALF_UP : -0.5 -> -1 +PHP_ROUND_HALF_DOWN : -0.5 -> -0 +PHP_ROUND_HALF_EVEN : -0.5 -> -0 +PHP_ROUND_HALF_ODD : -0.5 -> -1 diff --git a/ext/standard/tests/math/round_gh12143_4.phpt b/ext/standard/tests/math/round_gh12143_4.phpt new file mode 100644 index 0000000000000..d0f3166f1dc50 --- /dev/null +++ b/ext/standard/tests/math/round_gh12143_4.phpt @@ -0,0 +1,27 @@ +--TEST-- +GH-12143: Test rounding of 1.5. +--FILE-- + %+.17g\n", $mode, $number, round($number, 0, constant($mode))); + } +} +?> +--EXPECT-- +PHP_ROUND_HALF_UP : +1.5 -> +2 +PHP_ROUND_HALF_DOWN : +1.5 -> +1 +PHP_ROUND_HALF_EVEN : +1.5 -> +2 +PHP_ROUND_HALF_ODD : +1.5 -> +1 +PHP_ROUND_HALF_UP : -1.5 -> -2 +PHP_ROUND_HALF_DOWN : -1.5 -> -1 +PHP_ROUND_HALF_EVEN : -1.5 -> -2 +PHP_ROUND_HALF_ODD : -1.5 -> -1