Skip to content

FIX GH-12143: Remove pre-rounding #12291

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 11 additions & 81 deletions ext/standard/math.c
Original file line number Diff line number Diff line change
Expand Up @@ -30,50 +30,6 @@

#include "basic_functions.h"

/* {{{ php_intlog10abs
Returns floor(log10(fabs(val))), uses fast binary search */
static inline int php_intlog10abs(double value) {
value = fabs(value);

if (value < 1e-8 || value > 1e22) {
return (int)floor(log10(value));
} else {
/* Do a binary search with 5 steps */
int result = 15;
static const double values[] = {
1e-8, 1e-7, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1e0, 1e1, 1e2,
1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13,
1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};

if (value < values[result]) {
result -= 8;
} else {
result += 8;
}
if (value < values[result]) {
result -= 4;
} else {
result += 4;
}
if (value < values[result]) {
result -= 2;
} else {
result += 2;
}
if (value < values[result]) {
result -= 1;
} else {
result += 1;
}
if (value < values[result]) {
result -= 1;
}
result -= 8;
return result;
}
}
/* }}} */

/* {{{ php_intpow10
Returns pow(10.0, (double)power), uses fast lookup table for exact powers */
static inline double php_intpow10(int power) {
Expand Down Expand Up @@ -165,56 +121,30 @@ static inline double php_round_helper(double value, int mode) {

/* {{{ _php_math_round */
/*
* Rounds a number to a certain number of decimal places in a certain rounding
* mode. For the specifics of the algorithm, see http://wiki.php.net/rfc/rounding
* Rounds a number to a certain number of decimal places in a certain rounding mode.
* If you "HALF UP" a value like 0.285 (0.28499999999999998), it will be rounded to 0.28.
Copy link
Contributor

@mvorisek mvorisek Sep 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0.28499999999999998 is theoretical meaningless value, the stored accuracy is lower.

0.285 is simply 0.285, the rounding must match double to string representation /w precision set to -1.

https://3v4l.org/WiS71

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please see #12268

To be honest, I'm starting to feel like it's impossible for me to reconcile the divided opinions, and I'm starting to think about posting about this on internals.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see https://github.com/php/php-src/blob/php-8.2.10/Zend/zend_strtod.c#L3704 for double -> string conversion code /w mode=0, not sure if rounding thru string conversion will be fast enough, but in the end, all roundings must equal when does this way

*/
PHPAPI double _php_math_round(double value, int places, int mode) {
double f1, f2;
double f1;
double tmp_value;
int precision_places;

if (!zend_finite(value) || value == 0.0) {
return value;
}

places = places < INT_MIN+1 ? INT_MIN+1 : places;
precision_places = 14 - php_intlog10abs(value);

f1 = php_intpow10(abs(places));

/* If the decimal precision guaranteed by FP arithmetic is higher than
the requested places BUT is small enough to make sure a non-zero value
is returned, pre-round the result to the precision */
if (precision_places > places && precision_places - 15 < places) {
int64_t use_precision = precision_places < INT_MIN+1 ? INT_MIN+1 : precision_places;

f2 = php_intpow10(abs((int)use_precision));
if (use_precision >= 0) {
tmp_value = value * f2;
} else {
tmp_value = value / f2;
}
/* preround the result (tmp_value will always be something * 1e14,
thus never larger than 1e15 here) */
tmp_value = php_round_helper(tmp_value, mode);

use_precision = places - precision_places;
use_precision = use_precision < INT_MIN+1 ? INT_MIN+1 : use_precision;
/* now correctly move the decimal point */
f2 = php_intpow10(abs((int)use_precision));
/* because places < precision_places */
tmp_value = tmp_value / f2;
/* adjust the value */
if (places >= 0) {
tmp_value = value * f1;
} else {
/* adjust the value */
if (places >= 0) {
tmp_value = value * f1;
} else {
tmp_value = value / f1;
}
/* This value is beyond our precision, so rounding it is pointless */
if (fabs(tmp_value) >= 1e15) {
return value;
}
tmp_value = value / f1;
}
/* This value is beyond our precision, so rounding it is pointless */
if (fabs(tmp_value) >= 1e15) {
return value;
}

/* round the temp value */
Expand Down
74 changes: 60 additions & 14 deletions ext/standard/tests/math/bug24142.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,65 @@
Bug #24142 (round() problems)
--FILE--
<?php
$v = 0.005;
for ($i = 1; $i < 10; $i++) {
echo "round({$v}, 2) -> ".round($v, 2)."\n";
$v += 0.01;
}
echo "round(0.005, 2)\n";
var_dump(round(0.005, 2));
echo "\n";

echo "round(0.015, 2)\n";
var_dump(round(0.015, 2));
echo "\n";

echo "round(0.025, 2)\n";
var_dump(round(0.025, 2));
echo "\n";

echo "round(0.035, 2)\n";
var_dump(round(0.035, 2));
echo "\n";

echo "round(0.045, 2)\n";
var_dump(round(0.045, 2));
echo "\n";

echo "round(0.055, 2)\n";
var_dump(round(0.055, 2));
echo "\n";

echo "round(0.065, 2)\n";
var_dump(round(0.065, 2));
echo "\n";

echo "round(0.075, 2)\n";
var_dump(round(0.075, 2));
echo "\n";

echo "round(0.085, 2)\n";
var_dump(round(0.085, 2));
?>
--EXPECT--
round(0.005, 2) -> 0.01
round(0.015, 2) -> 0.02
round(0.025, 2) -> 0.03
round(0.035, 2) -> 0.04
round(0.045, 2) -> 0.05
round(0.055, 2) -> 0.06
round(0.065, 2) -> 0.07
round(0.075, 2) -> 0.08
round(0.085, 2) -> 0.09
round(0.005, 2)
float(0.01)

round(0.015, 2)
float(0.02)

round(0.025, 2)
float(0.03)

round(0.035, 2)
float(0.04)

round(0.045, 2)
float(0.05)

round(0.055, 2)
float(0.06)

round(0.065, 2)
float(0.07)

round(0.075, 2)
float(0.08)

round(0.085, 2)
float(0.09)
83 changes: 83 additions & 0 deletions ext/standard/tests/math/round_gh12143_remove_pre_rounding.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
--TEST--
Fix GH-12143: Remove pre-rounding
--FILE--
<?php
echo "HALF_UP\n";
var_dump(round(1.700000000000145, 13, PHP_ROUND_HALF_UP));
var_dump(round(-1.700000000000145, 13, PHP_ROUND_HALF_UP));
var_dump(round(123456789012344.5, -1, PHP_ROUND_HALF_UP));
var_dump(round(-123456789012344.5, -1, PHP_ROUND_HALF_UP));
var_dump (round (0.285, 2, PHP_ROUND_HALF_UP));
var_dump (round (-0.285, 2, PHP_ROUND_HALF_UP));
echo "\n";

echo "HALF_DOWN\n";
var_dump(round(1.70000000000015, 13, PHP_ROUND_HALF_DOWN));
var_dump(round(-1.70000000000015, 13, PHP_ROUND_HALF_DOWN));
var_dump(round(123456789012345.0, -1, PHP_ROUND_HALF_DOWN));
var_dump(round(-123456789012345.0, -1, PHP_ROUND_HALF_DOWN));
var_dump(round(1.500000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-1.500000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump (round (1.235, 2, PHP_ROUND_HALF_UP));
var_dump (round (-1.235, 2, PHP_ROUND_HALF_UP));
echo "\n";

echo "HALF_EVEN\n";
var_dump(round(1.70000000000025, 13, PHP_ROUND_HALF_EVEN));
var_dump(round(-1.70000000000025, 13, PHP_ROUND_HALF_EVEN));
var_dump(round(1.70000000000075, 13, PHP_ROUND_HALF_EVEN));
var_dump(round(-1.70000000000075, 13, PHP_ROUND_HALF_EVEN));
var_dump(round(12345678901234.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-12345678901234.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(1.500000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-1.500000000000001, 0, PHP_ROUND_HALF_EVEN));
echo "\n";

echo "HALF_ODD\n";
var_dump(round(1.70000000000025, 13, PHP_ROUND_HALF_ODD));
var_dump(round(-1.70000000000025, 13, PHP_ROUND_HALF_ODD));
var_dump(round(1.70000000000075, 13, PHP_ROUND_HALF_ODD));
var_dump(round(-1.70000000000075, 13, PHP_ROUND_HALF_ODD));
var_dump(round(12345678901233.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-12345678901233.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(1.500000000000001, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-1.500000000000001, 0, PHP_ROUND_HALF_ODD));
?>
--EXPECT--
HALF_UP
float(1.7000000000001)
float(-1.7000000000001)
float(123456789012340)
float(-123456789012340)
float(0.28)
float(-0.28)

HALF_DOWN
float(1.7000000000001)
float(-1.7000000000001)
float(123456789012340)
float(-123456789012340)
float(2)
float(-2)
float(1.24)
float(-1.24)

HALF_EVEN
float(1.7000000000002)
float(-1.7000000000002)
float(1.7000000000008)
float(-1.7000000000008)
float(12345678901234)
float(-12345678901234)
float(2)
float(-2)

HALF_ODD
float(1.7000000000003)
float(-1.7000000000003)
float(1.7000000000007)
float(-1.7000000000007)
float(12345678901233)
float(-12345678901233)
float(2)
float(-2)
10 changes: 0 additions & 10 deletions ext/standard/tests/math/round_prerounding.phpt

This file was deleted.