Fix #12143: Improve precision of rounding for FP numbers - #12159

Closed
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143
Closed

Fix #12143: Improve precision of rounding for FP numbers#12159
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143

Conversation

@jorgsowa

@jorgsowajorgsowa commented Sep 8, 2023

Copy link
Copy Markdown
Contributor

This PR fixes#12143

When we calculate FP numbers with large precision and perform floor(value + 0.5) or ceil(value + 0.5) we can round the result to the full integer which lead to incorrect result. We need to check if the number is not changed due to FP representation.

@jorgsowajorgsowa changed the title Fix #12123 Improve precision of rounding for large FP numbersFix #12123: Improve precision of rounding for FP numbersSep 9, 2023
@jorgsowa
jorgsowa marked this pull request as ready for review September 9, 2023 14:19
@jorgsowa
jorgsowa requested a review from bukka as a code ownerSeptember 9, 2023 14:19
Comment on lines +5 to +12
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_UP) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_UP) == -0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be better to dump the resulting number, rather than the result of the comparison.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Changed it. And also added cases for the number 0.5000000000000004.

@SakiTakamachi

SakiTakamachi commented Sep 9, 2023

Copy link
Copy Markdown
Member

@jorgsowa
Since this is a bug fix, the base branch should probably be PHP8.1 which is the oldest supported branch.
(This is my personal opinion, so please wait for maintainer's opinion before making your decision.)

@jorgsowajorgsowa changed the title Fix #12123: Improve precision of rounding for FP numbersFix #12143: Improve precision of rounding for FP numbersSep 10, 2023
@TimWolla

TimWolla commented Sep 10, 2023

Copy link
Copy Markdown
Member

For reference, here is a test program to easily check the behavior of php_round_helper with several numbers:

#include<math.h>#include<float.h>#include<stdio.h>#ifndefPHP_ROUND_HALF_UP#definePHP_ROUND_HALF_UP 0x01 /* Arithmetic rounding, up == away from zero */#endif#ifndefPHP_ROUND_HALF_DOWN#definePHP_ROUND_HALF_DOWN 0x02 /* Down == towards zero */#endif#ifndefPHP_ROUND_HALF_EVEN#definePHP_ROUND_HALF_EVEN 0x03 /* Banker's rounding */#endif#ifndefPHP_ROUND_HALF_ODD#definePHP_ROUND_HALF_ODD 0x04
#endifdoublephp_round_helper(doublevalue, intmode) {
doubletmp_value;
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;
}
}
returntmp_value;
}
intmain(void) {
doubled;
doublevalues[] = { 0.5, 1.5, 2.5, 3.5, 32.5, 64.5, 1024.5, 2048.5, 4096.5, 4503599627370494.5, 4503599627370495.5, 4503599627370496.5, 9007199254740989.5, 9007199254740990.5, 9007199254740991.5, 9007199254740992.5 };
for (size_ti=0; i<sizeof(values) / sizeof(values[0]); i++) {
doublevalue=values[i];
d=nextafter(value, 0); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=value; printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=nextafter(value, DBL_MAX); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
printf("\n");
}
}

Comment on lines +13 to +20
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[IMO]

The internal representation of double precision around 0.5 in IEEE754 and the corresponding FP are as follows:

3fdfffffffffffff // 0.49999999999999994
3fe0000000000000 // 0.5
3fe0000000000001 // 0.50000000000000001

Therefore, considering the purpose of the test, it would be better to set the value as follows(There were some places where digits were missing):

Suggested change
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We may also want to check 1.5 and 2.5 as well to confirm the operation of ODD and EVEN.

[FYI]

These internal representations and their corresponding FP are:

3ff7ffffffffffff // 1.4999999999999998
3ff8000000000000 // 1.5
3ff8000000000001 // 1.5000000000000002
4003ffffffffffff // 2.4999999999999996
4004000000000000 // 2.5
4004000000000001 // 2.5000000000000004

@bukka

bukka commented Sep 15, 2023

Copy link
Copy Markdown
Member

Doesn't this have the same effect as #12162 ? As I understand it the last part of condition is pretty much fallback to simple comparison check. Or will the previous mode specific checks have any sensible impact when used in pre-rounding?

I assume that the additional check is only useful in pre-rounding, right? If so, it might be a slight waste to do it all the time.

@SakiTakamachi

SakiTakamachi commented Sep 15, 2023

Copy link
Copy Markdown
Member

@bukka
This fixes improper rounding due to floating point precision issues.

For example, it is wrong to round 0.49999999999999994 to 1.
However, the existing processing looks like this:

0.49999999999999994 + 0.5
= 0.99999999999999994 // this is 3ff0000000000000 in IEEE754 with double precision.
= 1

Double precision makes it impossible to distinguish between 0.99999999999999994 and 1, so it is incorrectly rounded to 1.


Also, 0.49999999999999994 is a number that is not affected by #12162, which is a completely different problem.

@bukka

Copy link
Copy Markdown
Member

Ah ok I see now. It makes sense

TimWolla added a commit to TimWolla/php-src that referenced this pull request Sep 15, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see phpGH-12143 (this fixes one of the reported cases)
ClosesphpGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
TimWolla added a commit that referenced this pull request Sep 19, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see GH-12143 (this fixes one of the reported cases)
ClosesGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
@jorgsowa
jorgsowa deleted the fix-gh-12143 branch July 10, 2026 14:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect round($num, 0, PHP_ROUND_HALF_UP) result for $num = 1.4999999999999998 / 4503599627370495.5

4 participants

@jorgsowa@SakiTakamachi@TimWolla@bukka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix #12143: Improve precision of rounding for FP numbers - #12159

Closed
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143
Closed

Fix #12143: Improve precision of rounding for FP numbers#12159
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143

Conversation

@jorgsowa

@jorgsowajorgsowa commented Sep 8, 2023

Copy link
Copy Markdown
Contributor

This PR fixes#12143

When we calculate FP numbers with large precision and perform floor(value + 0.5) or ceil(value + 0.5) we can round the result to the full integer which lead to incorrect result. We need to check if the number is not changed due to FP representation.

@jorgsowajorgsowa changed the title Fix #12123 Improve precision of rounding for large FP numbersFix #12123: Improve precision of rounding for FP numbersSep 9, 2023
@jorgsowa
jorgsowa marked this pull request as ready for review September 9, 2023 14:19
@jorgsowa
jorgsowa requested a review from bukka as a code ownerSeptember 9, 2023 14:19
Comment on lines +5 to +12
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_UP) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_UP) == -0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be better to dump the resulting number, rather than the result of the comparison.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Changed it. And also added cases for the number 0.5000000000000004.

@SakiTakamachi

SakiTakamachi commented Sep 9, 2023

Copy link
Copy Markdown
Member

@jorgsowa
Since this is a bug fix, the base branch should probably be PHP8.1 which is the oldest supported branch.
(This is my personal opinion, so please wait for maintainer's opinion before making your decision.)

@jorgsowajorgsowa changed the title Fix #12123: Improve precision of rounding for FP numbersFix #12143: Improve precision of rounding for FP numbersSep 10, 2023
@TimWolla

TimWolla commented Sep 10, 2023

Copy link
Copy Markdown
Member

For reference, here is a test program to easily check the behavior of php_round_helper with several numbers:

#include<math.h>#include<float.h>#include<stdio.h>#ifndefPHP_ROUND_HALF_UP#definePHP_ROUND_HALF_UP 0x01 /* Arithmetic rounding, up == away from zero */#endif#ifndefPHP_ROUND_HALF_DOWN#definePHP_ROUND_HALF_DOWN 0x02 /* Down == towards zero */#endif#ifndefPHP_ROUND_HALF_EVEN#definePHP_ROUND_HALF_EVEN 0x03 /* Banker's rounding */#endif#ifndefPHP_ROUND_HALF_ODD#definePHP_ROUND_HALF_ODD 0x04
#endifdoublephp_round_helper(doublevalue, intmode) {
doubletmp_value;
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;
}
}
returntmp_value;
}
intmain(void) {
doubled;
doublevalues[] = { 0.5, 1.5, 2.5, 3.5, 32.5, 64.5, 1024.5, 2048.5, 4096.5, 4503599627370494.5, 4503599627370495.5, 4503599627370496.5, 9007199254740989.5, 9007199254740990.5, 9007199254740991.5, 9007199254740992.5 };
for (size_ti=0; i<sizeof(values) / sizeof(values[0]); i++) {
doublevalue=values[i];
d=nextafter(value, 0); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=value; printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=nextafter(value, DBL_MAX); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
printf("\n");
}
}

Comment on lines +13 to +20
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[IMO]

The internal representation of double precision around 0.5 in IEEE754 and the corresponding FP are as follows:

3fdfffffffffffff // 0.49999999999999994
3fe0000000000000 // 0.5
3fe0000000000001 // 0.50000000000000001

Therefore, considering the purpose of the test, it would be better to set the value as follows(There were some places where digits were missing):

Suggested change
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We may also want to check 1.5 and 2.5 as well to confirm the operation of ODD and EVEN.

[FYI]

These internal representations and their corresponding FP are:

3ff7ffffffffffff // 1.4999999999999998
3ff8000000000000 // 1.5
3ff8000000000001 // 1.5000000000000002
4003ffffffffffff // 2.4999999999999996
4004000000000000 // 2.5
4004000000000001 // 2.5000000000000004

@bukka

bukka commented Sep 15, 2023

Copy link
Copy Markdown
Member

Doesn't this have the same effect as #12162 ? As I understand it the last part of condition is pretty much fallback to simple comparison check. Or will the previous mode specific checks have any sensible impact when used in pre-rounding?

I assume that the additional check is only useful in pre-rounding, right? If so, it might be a slight waste to do it all the time.

@SakiTakamachi

SakiTakamachi commented Sep 15, 2023

Copy link
Copy Markdown
Member

@bukka
This fixes improper rounding due to floating point precision issues.

For example, it is wrong to round 0.49999999999999994 to 1.
However, the existing processing looks like this:

0.49999999999999994 + 0.5
= 0.99999999999999994 // this is 3ff0000000000000 in IEEE754 with double precision.
= 1

Double precision makes it impossible to distinguish between 0.99999999999999994 and 1, so it is incorrectly rounded to 1.


Also, 0.49999999999999994 is a number that is not affected by #12162, which is a completely different problem.

@bukka

Copy link
Copy Markdown
Member

Ah ok I see now. It makes sense

TimWolla added a commit to TimWolla/php-src that referenced this pull request Sep 15, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see phpGH-12143 (this fixes one of the reported cases)
ClosesphpGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
TimWolla added a commit that referenced this pull request Sep 19, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see GH-12143 (this fixes one of the reported cases)
ClosesGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
@jorgsowa
jorgsowa deleted the fix-gh-12143 branch July 10, 2026 14:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect round($num, 0, PHP_ROUND_HALF_UP) result for $num = 1.4999999999999998 / 4503599627370495.5

4 participants

@jorgsowa@SakiTakamachi@TimWolla@bukka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix #12143: Improve precision of rounding for FP numbers - #12159

Closed
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143
Closed

Fix #12143: Improve precision of rounding for FP numbers#12159
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143

Conversation

@jorgsowa

@jorgsowajorgsowa commented Sep 8, 2023

Copy link
Copy Markdown
Contributor

This PR fixes#12143

When we calculate FP numbers with large precision and perform floor(value + 0.5) or ceil(value + 0.5) we can round the result to the full integer which lead to incorrect result. We need to check if the number is not changed due to FP representation.

@jorgsowajorgsowa changed the title Fix #12123 Improve precision of rounding for large FP numbersFix #12123: Improve precision of rounding for FP numbersSep 9, 2023
@jorgsowa
jorgsowa marked this pull request as ready for review September 9, 2023 14:19
@jorgsowa
jorgsowa requested a review from bukka as a code ownerSeptember 9, 2023 14:19
Comment on lines +5 to +12
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_UP) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_UP) == -0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be better to dump the resulting number, rather than the result of the comparison.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Changed it. And also added cases for the number 0.5000000000000004.

@SakiTakamachi

SakiTakamachi commented Sep 9, 2023

Copy link
Copy Markdown
Member

@jorgsowa
Since this is a bug fix, the base branch should probably be PHP8.1 which is the oldest supported branch.
(This is my personal opinion, so please wait for maintainer's opinion before making your decision.)

@jorgsowajorgsowa changed the title Fix #12123: Improve precision of rounding for FP numbersFix #12143: Improve precision of rounding for FP numbersSep 10, 2023
@TimWolla

TimWolla commented Sep 10, 2023

Copy link
Copy Markdown
Member

For reference, here is a test program to easily check the behavior of php_round_helper with several numbers:

#include<math.h>#include<float.h>#include<stdio.h>#ifndefPHP_ROUND_HALF_UP#definePHP_ROUND_HALF_UP 0x01 /* Arithmetic rounding, up == away from zero */#endif#ifndefPHP_ROUND_HALF_DOWN#definePHP_ROUND_HALF_DOWN 0x02 /* Down == towards zero */#endif#ifndefPHP_ROUND_HALF_EVEN#definePHP_ROUND_HALF_EVEN 0x03 /* Banker's rounding */#endif#ifndefPHP_ROUND_HALF_ODD#definePHP_ROUND_HALF_ODD 0x04
#endifdoublephp_round_helper(doublevalue, intmode) {
doubletmp_value;
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;
}
}
returntmp_value;
}
intmain(void) {
doubled;
doublevalues[] = { 0.5, 1.5, 2.5, 3.5, 32.5, 64.5, 1024.5, 2048.5, 4096.5, 4503599627370494.5, 4503599627370495.5, 4503599627370496.5, 9007199254740989.5, 9007199254740990.5, 9007199254740991.5, 9007199254740992.5 };
for (size_ti=0; i<sizeof(values) / sizeof(values[0]); i++) {
doublevalue=values[i];
d=nextafter(value, 0); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=value; printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=nextafter(value, DBL_MAX); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
printf("\n");
}
}

Comment on lines +13 to +20
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[IMO]

The internal representation of double precision around 0.5 in IEEE754 and the corresponding FP are as follows:

3fdfffffffffffff // 0.49999999999999994
3fe0000000000000 // 0.5
3fe0000000000001 // 0.50000000000000001

Therefore, considering the purpose of the test, it would be better to set the value as follows(There were some places where digits were missing):

Suggested change
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We may also want to check 1.5 and 2.5 as well to confirm the operation of ODD and EVEN.

[FYI]

These internal representations and their corresponding FP are:

3ff7ffffffffffff // 1.4999999999999998
3ff8000000000000 // 1.5
3ff8000000000001 // 1.5000000000000002
4003ffffffffffff // 2.4999999999999996
4004000000000000 // 2.5
4004000000000001 // 2.5000000000000004

@bukka

bukka commented Sep 15, 2023

Copy link
Copy Markdown
Member

Doesn't this have the same effect as #12162 ? As I understand it the last part of condition is pretty much fallback to simple comparison check. Or will the previous mode specific checks have any sensible impact when used in pre-rounding?

I assume that the additional check is only useful in pre-rounding, right? If so, it might be a slight waste to do it all the time.

@SakiTakamachi

SakiTakamachi commented Sep 15, 2023

Copy link
Copy Markdown
Member

@bukka
This fixes improper rounding due to floating point precision issues.

For example, it is wrong to round 0.49999999999999994 to 1.
However, the existing processing looks like this:

0.49999999999999994 + 0.5
= 0.99999999999999994 // this is 3ff0000000000000 in IEEE754 with double precision.
= 1

Double precision makes it impossible to distinguish between 0.99999999999999994 and 1, so it is incorrectly rounded to 1.


Also, 0.49999999999999994 is a number that is not affected by #12162, which is a completely different problem.

@bukka

Copy link
Copy Markdown
Member

Ah ok I see now. It makes sense

TimWolla added a commit to TimWolla/php-src that referenced this pull request Sep 15, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see phpGH-12143 (this fixes one of the reported cases)
ClosesphpGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
TimWolla added a commit that referenced this pull request Sep 19, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see GH-12143 (this fixes one of the reported cases)
ClosesGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
@jorgsowa
jorgsowa deleted the fix-gh-12143 branch July 10, 2026 14:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect round($num, 0, PHP_ROUND_HALF_UP) result for $num = 1.4999999999999998 / 4503599627370495.5

4 participants

@jorgsowa@SakiTakamachi@TimWolla@bukka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix #12143: Improve precision of rounding for FP numbers - #12159

Closed
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143
Closed

Fix #12143: Improve precision of rounding for FP numbers#12159
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143

Conversation

@jorgsowa

@jorgsowajorgsowa commented Sep 8, 2023

Copy link
Copy Markdown
Contributor

This PR fixes#12143

When we calculate FP numbers with large precision and perform floor(value + 0.5) or ceil(value + 0.5) we can round the result to the full integer which lead to incorrect result. We need to check if the number is not changed due to FP representation.

@jorgsowajorgsowa changed the title Fix #12123 Improve precision of rounding for large FP numbersFix #12123: Improve precision of rounding for FP numbersSep 9, 2023
@jorgsowa
jorgsowa marked this pull request as ready for review September 9, 2023 14:19
@jorgsowa
jorgsowa requested a review from bukka as a code ownerSeptember 9, 2023 14:19
Comment on lines +5 to +12
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_UP) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_UP) == -0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be better to dump the resulting number, rather than the result of the comparison.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Changed it. And also added cases for the number 0.5000000000000004.

@SakiTakamachi

SakiTakamachi commented Sep 9, 2023

Copy link
Copy Markdown
Member

@jorgsowa
Since this is a bug fix, the base branch should probably be PHP8.1 which is the oldest supported branch.
(This is my personal opinion, so please wait for maintainer's opinion before making your decision.)

@jorgsowajorgsowa changed the title Fix #12123: Improve precision of rounding for FP numbersFix #12143: Improve precision of rounding for FP numbersSep 10, 2023
@TimWolla

TimWolla commented Sep 10, 2023

Copy link
Copy Markdown
Member

For reference, here is a test program to easily check the behavior of php_round_helper with several numbers:

#include<math.h>#include<float.h>#include<stdio.h>#ifndefPHP_ROUND_HALF_UP#definePHP_ROUND_HALF_UP 0x01 /* Arithmetic rounding, up == away from zero */#endif#ifndefPHP_ROUND_HALF_DOWN#definePHP_ROUND_HALF_DOWN 0x02 /* Down == towards zero */#endif#ifndefPHP_ROUND_HALF_EVEN#definePHP_ROUND_HALF_EVEN 0x03 /* Banker's rounding */#endif#ifndefPHP_ROUND_HALF_ODD#definePHP_ROUND_HALF_ODD 0x04
#endifdoublephp_round_helper(doublevalue, intmode) {
doubletmp_value;
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;
}
}
returntmp_value;
}
intmain(void) {
doubled;
doublevalues[] = { 0.5, 1.5, 2.5, 3.5, 32.5, 64.5, 1024.5, 2048.5, 4096.5, 4503599627370494.5, 4503599627370495.5, 4503599627370496.5, 9007199254740989.5, 9007199254740990.5, 9007199254740991.5, 9007199254740992.5 };
for (size_ti=0; i<sizeof(values) / sizeof(values[0]); i++) {
doublevalue=values[i];
d=nextafter(value, 0); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=value; printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=nextafter(value, DBL_MAX); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
printf("\n");
}
}

Comment on lines +13 to +20
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[IMO]

The internal representation of double precision around 0.5 in IEEE754 and the corresponding FP are as follows:

3fdfffffffffffff // 0.49999999999999994
3fe0000000000000 // 0.5
3fe0000000000001 // 0.50000000000000001

Therefore, considering the purpose of the test, it would be better to set the value as follows(There were some places where digits were missing):

Suggested change
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We may also want to check 1.5 and 2.5 as well to confirm the operation of ODD and EVEN.

[FYI]

These internal representations and their corresponding FP are:

3ff7ffffffffffff // 1.4999999999999998
3ff8000000000000 // 1.5
3ff8000000000001 // 1.5000000000000002
4003ffffffffffff // 2.4999999999999996
4004000000000000 // 2.5
4004000000000001 // 2.5000000000000004

@bukka

bukka commented Sep 15, 2023

Copy link
Copy Markdown
Member

Doesn't this have the same effect as #12162 ? As I understand it the last part of condition is pretty much fallback to simple comparison check. Or will the previous mode specific checks have any sensible impact when used in pre-rounding?

I assume that the additional check is only useful in pre-rounding, right? If so, it might be a slight waste to do it all the time.

@SakiTakamachi

SakiTakamachi commented Sep 15, 2023

Copy link
Copy Markdown
Member

@bukka
This fixes improper rounding due to floating point precision issues.

For example, it is wrong to round 0.49999999999999994 to 1.
However, the existing processing looks like this:

0.49999999999999994 + 0.5
= 0.99999999999999994 // this is 3ff0000000000000 in IEEE754 with double precision.
= 1

Double precision makes it impossible to distinguish between 0.99999999999999994 and 1, so it is incorrectly rounded to 1.


Also, 0.49999999999999994 is a number that is not affected by #12162, which is a completely different problem.

@bukka

Copy link
Copy Markdown
Member

Ah ok I see now. It makes sense

TimWolla added a commit to TimWolla/php-src that referenced this pull request Sep 15, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see phpGH-12143 (this fixes one of the reported cases)
ClosesphpGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
TimWolla added a commit that referenced this pull request Sep 19, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see GH-12143 (this fixes one of the reported cases)
ClosesGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
@jorgsowa
jorgsowa deleted the fix-gh-12143 branch July 10, 2026 14:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect round($num, 0, PHP_ROUND_HALF_UP) result for $num = 1.4999999999999998 / 4503599627370495.5

4 participants

@jorgsowa@SakiTakamachi@TimWolla@bukka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix #12143: Improve precision of rounding for FP numbers - #12159

Closed
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143
Closed

Fix #12143: Improve precision of rounding for FP numbers#12159
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143

Conversation

@jorgsowa

@jorgsowajorgsowa commented Sep 8, 2023

Copy link
Copy Markdown
Contributor

This PR fixes#12143

When we calculate FP numbers with large precision and perform floor(value + 0.5) or ceil(value + 0.5) we can round the result to the full integer which lead to incorrect result. We need to check if the number is not changed due to FP representation.

@jorgsowajorgsowa changed the title Fix #12123 Improve precision of rounding for large FP numbersFix #12123: Improve precision of rounding for FP numbersSep 9, 2023
@jorgsowa
jorgsowa marked this pull request as ready for review September 9, 2023 14:19
@jorgsowa
jorgsowa requested a review from bukka as a code ownerSeptember 9, 2023 14:19
Comment on lines +5 to +12
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_UP) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_UP) == -0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be better to dump the resulting number, rather than the result of the comparison.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Changed it. And also added cases for the number 0.5000000000000004.

@SakiTakamachi

SakiTakamachi commented Sep 9, 2023

Copy link
Copy Markdown
Member

@jorgsowa
Since this is a bug fix, the base branch should probably be PHP8.1 which is the oldest supported branch.
(This is my personal opinion, so please wait for maintainer's opinion before making your decision.)

@jorgsowajorgsowa changed the title Fix #12123: Improve precision of rounding for FP numbersFix #12143: Improve precision of rounding for FP numbersSep 10, 2023
@TimWolla

TimWolla commented Sep 10, 2023

Copy link
Copy Markdown
Member

For reference, here is a test program to easily check the behavior of php_round_helper with several numbers:

#include<math.h>#include<float.h>#include<stdio.h>#ifndefPHP_ROUND_HALF_UP#definePHP_ROUND_HALF_UP 0x01 /* Arithmetic rounding, up == away from zero */#endif#ifndefPHP_ROUND_HALF_DOWN#definePHP_ROUND_HALF_DOWN 0x02 /* Down == towards zero */#endif#ifndefPHP_ROUND_HALF_EVEN#definePHP_ROUND_HALF_EVEN 0x03 /* Banker's rounding */#endif#ifndefPHP_ROUND_HALF_ODD#definePHP_ROUND_HALF_ODD 0x04
#endifdoublephp_round_helper(doublevalue, intmode) {
doubletmp_value;
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;
}
}
returntmp_value;
}
intmain(void) {
doubled;
doublevalues[] = { 0.5, 1.5, 2.5, 3.5, 32.5, 64.5, 1024.5, 2048.5, 4096.5, 4503599627370494.5, 4503599627370495.5, 4503599627370496.5, 9007199254740989.5, 9007199254740990.5, 9007199254740991.5, 9007199254740992.5 };
for (size_ti=0; i<sizeof(values) / sizeof(values[0]); i++) {
doublevalue=values[i];
d=nextafter(value, 0); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=value; printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=nextafter(value, DBL_MAX); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
printf("\n");
}
}

Comment on lines +13 to +20
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[IMO]

The internal representation of double precision around 0.5 in IEEE754 and the corresponding FP are as follows:

3fdfffffffffffff // 0.49999999999999994
3fe0000000000000 // 0.5
3fe0000000000001 // 0.50000000000000001

Therefore, considering the purpose of the test, it would be better to set the value as follows(There were some places where digits were missing):

Suggested change
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We may also want to check 1.5 and 2.5 as well to confirm the operation of ODD and EVEN.

[FYI]

These internal representations and their corresponding FP are:

3ff7ffffffffffff // 1.4999999999999998
3ff8000000000000 // 1.5
3ff8000000000001 // 1.5000000000000002
4003ffffffffffff // 2.4999999999999996
4004000000000000 // 2.5
4004000000000001 // 2.5000000000000004

@bukka

bukka commented Sep 15, 2023

Copy link
Copy Markdown
Member

Doesn't this have the same effect as #12162 ? As I understand it the last part of condition is pretty much fallback to simple comparison check. Or will the previous mode specific checks have any sensible impact when used in pre-rounding?

I assume that the additional check is only useful in pre-rounding, right? If so, it might be a slight waste to do it all the time.

@SakiTakamachi

SakiTakamachi commented Sep 15, 2023

Copy link
Copy Markdown
Member

@bukka
This fixes improper rounding due to floating point precision issues.

For example, it is wrong to round 0.49999999999999994 to 1.
However, the existing processing looks like this:

0.49999999999999994 + 0.5
= 0.99999999999999994 // this is 3ff0000000000000 in IEEE754 with double precision.
= 1

Double precision makes it impossible to distinguish between 0.99999999999999994 and 1, so it is incorrectly rounded to 1.


Also, 0.49999999999999994 is a number that is not affected by #12162, which is a completely different problem.

@bukka

Copy link
Copy Markdown
Member

Ah ok I see now. It makes sense

TimWolla added a commit to TimWolla/php-src that referenced this pull request Sep 15, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see phpGH-12143 (this fixes one of the reported cases)
ClosesphpGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
TimWolla added a commit that referenced this pull request Sep 19, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see GH-12143 (this fixes one of the reported cases)
ClosesGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
@jorgsowa
jorgsowa deleted the fix-gh-12143 branch July 10, 2026 14:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect round($num, 0, PHP_ROUND_HALF_UP) result for $num = 1.4999999999999998 / 4503599627370495.5

4 participants

@jorgsowa@SakiTakamachi@TimWolla@bukka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix #12143: Improve precision of rounding for FP numbers - #12159

Closed
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143
Closed

Fix #12143: Improve precision of rounding for FP numbers#12159
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143

Conversation

@jorgsowa

@jorgsowajorgsowa commented Sep 8, 2023

Copy link
Copy Markdown
Contributor

This PR fixes#12143

When we calculate FP numbers with large precision and perform floor(value + 0.5) or ceil(value + 0.5) we can round the result to the full integer which lead to incorrect result. We need to check if the number is not changed due to FP representation.

@jorgsowajorgsowa changed the title Fix #12123 Improve precision of rounding for large FP numbersFix #12123: Improve precision of rounding for FP numbersSep 9, 2023
@jorgsowa
jorgsowa marked this pull request as ready for review September 9, 2023 14:19
@jorgsowa
jorgsowa requested a review from bukka as a code ownerSeptember 9, 2023 14:19
Comment on lines +5 to +12
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_UP) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_UP) == -0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be better to dump the resulting number, rather than the result of the comparison.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Changed it. And also added cases for the number 0.5000000000000004.

@SakiTakamachi

SakiTakamachi commented Sep 9, 2023

Copy link
Copy Markdown
Member

@jorgsowa
Since this is a bug fix, the base branch should probably be PHP8.1 which is the oldest supported branch.
(This is my personal opinion, so please wait for maintainer's opinion before making your decision.)

@jorgsowajorgsowa changed the title Fix #12123: Improve precision of rounding for FP numbersFix #12143: Improve precision of rounding for FP numbersSep 10, 2023
@TimWolla

TimWolla commented Sep 10, 2023

Copy link
Copy Markdown
Member

For reference, here is a test program to easily check the behavior of php_round_helper with several numbers:

#include<math.h>#include<float.h>#include<stdio.h>#ifndefPHP_ROUND_HALF_UP#definePHP_ROUND_HALF_UP 0x01 /* Arithmetic rounding, up == away from zero */#endif#ifndefPHP_ROUND_HALF_DOWN#definePHP_ROUND_HALF_DOWN 0x02 /* Down == towards zero */#endif#ifndefPHP_ROUND_HALF_EVEN#definePHP_ROUND_HALF_EVEN 0x03 /* Banker's rounding */#endif#ifndefPHP_ROUND_HALF_ODD#definePHP_ROUND_HALF_ODD 0x04
#endifdoublephp_round_helper(doublevalue, intmode) {
doubletmp_value;
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;
}
}
returntmp_value;
}
intmain(void) {
doubled;
doublevalues[] = { 0.5, 1.5, 2.5, 3.5, 32.5, 64.5, 1024.5, 2048.5, 4096.5, 4503599627370494.5, 4503599627370495.5, 4503599627370496.5, 9007199254740989.5, 9007199254740990.5, 9007199254740991.5, 9007199254740992.5 };
for (size_ti=0; i<sizeof(values) / sizeof(values[0]); i++) {
doublevalue=values[i];
d=nextafter(value, 0); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=value; printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=nextafter(value, DBL_MAX); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
printf("\n");
}
}

Comment on lines +13 to +20
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[IMO]

The internal representation of double precision around 0.5 in IEEE754 and the corresponding FP are as follows:

3fdfffffffffffff // 0.49999999999999994
3fe0000000000000 // 0.5
3fe0000000000001 // 0.50000000000000001

Therefore, considering the purpose of the test, it would be better to set the value as follows(There were some places where digits were missing):

Suggested change
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We may also want to check 1.5 and 2.5 as well to confirm the operation of ODD and EVEN.

[FYI]

These internal representations and their corresponding FP are:

3ff7ffffffffffff // 1.4999999999999998
3ff8000000000000 // 1.5
3ff8000000000001 // 1.5000000000000002
4003ffffffffffff // 2.4999999999999996
4004000000000000 // 2.5
4004000000000001 // 2.5000000000000004

@bukka

bukka commented Sep 15, 2023

Copy link
Copy Markdown
Member

Doesn't this have the same effect as #12162 ? As I understand it the last part of condition is pretty much fallback to simple comparison check. Or will the previous mode specific checks have any sensible impact when used in pre-rounding?

I assume that the additional check is only useful in pre-rounding, right? If so, it might be a slight waste to do it all the time.

@SakiTakamachi

SakiTakamachi commented Sep 15, 2023

Copy link
Copy Markdown
Member

@bukka
This fixes improper rounding due to floating point precision issues.

For example, it is wrong to round 0.49999999999999994 to 1.
However, the existing processing looks like this:

0.49999999999999994 + 0.5
= 0.99999999999999994 // this is 3ff0000000000000 in IEEE754 with double precision.
= 1

Double precision makes it impossible to distinguish between 0.99999999999999994 and 1, so it is incorrectly rounded to 1.


Also, 0.49999999999999994 is a number that is not affected by #12162, which is a completely different problem.

@bukka

Copy link
Copy Markdown
Member

Ah ok I see now. It makes sense

TimWolla added a commit to TimWolla/php-src that referenced this pull request Sep 15, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see phpGH-12143 (this fixes one of the reported cases)
ClosesphpGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
TimWolla added a commit that referenced this pull request Sep 19, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see GH-12143 (this fixes one of the reported cases)
ClosesGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
@jorgsowa
jorgsowa deleted the fix-gh-12143 branch July 10, 2026 14:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect round($num, 0, PHP_ROUND_HALF_UP) result for $num = 1.4999999999999998 / 4503599627370495.5

4 participants

@jorgsowa@SakiTakamachi@TimWolla@bukka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix #12143: Improve precision of rounding for FP numbers - #12159

Closed
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143
Closed

Fix #12143: Improve precision of rounding for FP numbers#12159
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143

Conversation

@jorgsowa

@jorgsowajorgsowa commented Sep 8, 2023

Copy link
Copy Markdown
Contributor

This PR fixes#12143

When we calculate FP numbers with large precision and perform floor(value + 0.5) or ceil(value + 0.5) we can round the result to the full integer which lead to incorrect result. We need to check if the number is not changed due to FP representation.

@jorgsowajorgsowa changed the title Fix #12123 Improve precision of rounding for large FP numbersFix #12123: Improve precision of rounding for FP numbersSep 9, 2023
@jorgsowa
jorgsowa marked this pull request as ready for review September 9, 2023 14:19
@jorgsowa
jorgsowa requested a review from bukka as a code ownerSeptember 9, 2023 14:19
Comment on lines +5 to +12
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_UP) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_UP) == -0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be better to dump the resulting number, rather than the result of the comparison.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Changed it. And also added cases for the number 0.5000000000000004.

@SakiTakamachi

SakiTakamachi commented Sep 9, 2023

Copy link
Copy Markdown
Member

@jorgsowa
Since this is a bug fix, the base branch should probably be PHP8.1 which is the oldest supported branch.
(This is my personal opinion, so please wait for maintainer's opinion before making your decision.)

@jorgsowajorgsowa changed the title Fix #12123: Improve precision of rounding for FP numbersFix #12143: Improve precision of rounding for FP numbersSep 10, 2023
@TimWolla

TimWolla commented Sep 10, 2023

Copy link
Copy Markdown
Member

For reference, here is a test program to easily check the behavior of php_round_helper with several numbers:

#include<math.h>#include<float.h>#include<stdio.h>#ifndefPHP_ROUND_HALF_UP#definePHP_ROUND_HALF_UP 0x01 /* Arithmetic rounding, up == away from zero */#endif#ifndefPHP_ROUND_HALF_DOWN#definePHP_ROUND_HALF_DOWN 0x02 /* Down == towards zero */#endif#ifndefPHP_ROUND_HALF_EVEN#definePHP_ROUND_HALF_EVEN 0x03 /* Banker's rounding */#endif#ifndefPHP_ROUND_HALF_ODD#definePHP_ROUND_HALF_ODD 0x04
#endifdoublephp_round_helper(doublevalue, intmode) {
doubletmp_value;
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;
}
}
returntmp_value;
}
intmain(void) {
doubled;
doublevalues[] = { 0.5, 1.5, 2.5, 3.5, 32.5, 64.5, 1024.5, 2048.5, 4096.5, 4503599627370494.5, 4503599627370495.5, 4503599627370496.5, 9007199254740989.5, 9007199254740990.5, 9007199254740991.5, 9007199254740992.5 };
for (size_ti=0; i<sizeof(values) / sizeof(values[0]); i++) {
doublevalue=values[i];
d=nextafter(value, 0); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=value; printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=nextafter(value, DBL_MAX); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
printf("\n");
}
}

Comment on lines +13 to +20
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[IMO]

The internal representation of double precision around 0.5 in IEEE754 and the corresponding FP are as follows:

3fdfffffffffffff // 0.49999999999999994
3fe0000000000000 // 0.5
3fe0000000000001 // 0.50000000000000001

Therefore, considering the purpose of the test, it would be better to set the value as follows(There were some places where digits were missing):

Suggested change
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We may also want to check 1.5 and 2.5 as well to confirm the operation of ODD and EVEN.

[FYI]

These internal representations and their corresponding FP are:

3ff7ffffffffffff // 1.4999999999999998
3ff8000000000000 // 1.5
3ff8000000000001 // 1.5000000000000002
4003ffffffffffff // 2.4999999999999996
4004000000000000 // 2.5
4004000000000001 // 2.5000000000000004

@bukka

bukka commented Sep 15, 2023

Copy link
Copy Markdown
Member

Doesn't this have the same effect as #12162 ? As I understand it the last part of condition is pretty much fallback to simple comparison check. Or will the previous mode specific checks have any sensible impact when used in pre-rounding?

I assume that the additional check is only useful in pre-rounding, right? If so, it might be a slight waste to do it all the time.

@SakiTakamachi

SakiTakamachi commented Sep 15, 2023

Copy link
Copy Markdown
Member

@bukka
This fixes improper rounding due to floating point precision issues.

For example, it is wrong to round 0.49999999999999994 to 1.
However, the existing processing looks like this:

0.49999999999999994 + 0.5
= 0.99999999999999994 // this is 3ff0000000000000 in IEEE754 with double precision.
= 1

Double precision makes it impossible to distinguish between 0.99999999999999994 and 1, so it is incorrectly rounded to 1.


Also, 0.49999999999999994 is a number that is not affected by #12162, which is a completely different problem.

@bukka

Copy link
Copy Markdown
Member

Ah ok I see now. It makes sense

TimWolla added a commit to TimWolla/php-src that referenced this pull request Sep 15, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see phpGH-12143 (this fixes one of the reported cases)
ClosesphpGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
TimWolla added a commit that referenced this pull request Sep 19, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see GH-12143 (this fixes one of the reported cases)
ClosesGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
@jorgsowa
jorgsowa deleted the fix-gh-12143 branch July 10, 2026 14:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect round($num, 0, PHP_ROUND_HALF_UP) result for $num = 1.4999999999999998 / 4503599627370495.5

4 participants

@jorgsowa@SakiTakamachi@TimWolla@bukka
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix #12143: Improve precision of rounding for FP numbers - #12159

Closed
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143
Closed

Fix #12143: Improve precision of rounding for FP numbers#12159
jorgsowa wants to merge 5 commits into
php:masterfrom
jorgsowa:fix-gh-12143

Conversation

@jorgsowa

@jorgsowajorgsowa commented Sep 8, 2023

Copy link
Copy Markdown
Contributor

This PR fixes#12143

When we calculate FP numbers with large precision and perform floor(value + 0.5) or ceil(value + 0.5) we can round the result to the full integer which lead to incorrect result. We need to check if the number is not changed due to FP representation.

@jorgsowajorgsowa changed the title Fix #12123 Improve precision of rounding for large FP numbersFix #12123: Improve precision of rounding for FP numbersSep 9, 2023
@jorgsowa
jorgsowa marked this pull request as ready for review September 9, 2023 14:19
@jorgsowa
jorgsowa requested a review from bukka as a code ownerSeptember 9, 2023 14:19
Comment on lines +5 to +12
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_UP) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_UP) == -0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_DOWN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_EVEN) == 0);
var_dump(round(-0.49999999999999994, 0, PHP_ROUND_HALF_ODD) == 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It might be better to dump the resulting number, rather than the result of the comparison.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Changed it. And also added cases for the number 0.5000000000000004.

@SakiTakamachi

SakiTakamachi commented Sep 9, 2023

Copy link
Copy Markdown
Member

@jorgsowa
Since this is a bug fix, the base branch should probably be PHP8.1 which is the oldest supported branch.
(This is my personal opinion, so please wait for maintainer's opinion before making your decision.)

@jorgsowajorgsowa changed the title Fix #12123: Improve precision of rounding for FP numbersFix #12143: Improve precision of rounding for FP numbersSep 10, 2023
@TimWolla

TimWolla commented Sep 10, 2023

Copy link
Copy Markdown
Member

For reference, here is a test program to easily check the behavior of php_round_helper with several numbers:

#include<math.h>#include<float.h>#include<stdio.h>#ifndefPHP_ROUND_HALF_UP#definePHP_ROUND_HALF_UP 0x01 /* Arithmetic rounding, up == away from zero */#endif#ifndefPHP_ROUND_HALF_DOWN#definePHP_ROUND_HALF_DOWN 0x02 /* Down == towards zero */#endif#ifndefPHP_ROUND_HALF_EVEN#definePHP_ROUND_HALF_EVEN 0x03 /* Banker's rounding */#endif#ifndefPHP_ROUND_HALF_ODD#definePHP_ROUND_HALF_ODD 0x04
#endifdoublephp_round_helper(doublevalue, intmode) {
doubletmp_value;
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;
}
}
returntmp_value;
}
intmain(void) {
doubled;
doublevalues[] = { 0.5, 1.5, 2.5, 3.5, 32.5, 64.5, 1024.5, 2048.5, 4096.5, 4503599627370494.5, 4503599627370495.5, 4503599627370496.5, 9007199254740989.5, 9007199254740990.5, 9007199254740991.5, 9007199254740992.5 };
for (size_ti=0; i<sizeof(values) / sizeof(values[0]); i++) {
doublevalue=values[i];
d=nextafter(value, 0); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=value; printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
d=nextafter(value, DBL_MAX); printf("%.17f %.17f %.17f\n", d, php_round_helper(d, PHP_ROUND_HALF_UP), php_round_helper(d, PHP_ROUND_HALF_DOWN));
printf("\n");
}
}

Comment on lines +13 to +20
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[IMO]

The internal representation of double precision around 0.5 in IEEE754 and the corresponding FP are as follows:

3fdfffffffffffff // 0.49999999999999994
3fe0000000000000 // 0.5
3fe0000000000001 // 0.50000000000000001

Therefore, considering the purpose of the test, it would be better to set the value as follows(There were some places where digits were missing):

Suggested change
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5000000000000004, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.5, 0, PHP_ROUND_HALF_ODD));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(0.50000000000000001, 0, PHP_ROUND_HALF_ODD));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_UP));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_DOWN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_EVEN));
var_dump(round(-0.50000000000000001, 0, PHP_ROUND_HALF_ODD));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We may also want to check 1.5 and 2.5 as well to confirm the operation of ODD and EVEN.

[FYI]

These internal representations and their corresponding FP are:

3ff7ffffffffffff // 1.4999999999999998
3ff8000000000000 // 1.5
3ff8000000000001 // 1.5000000000000002
4003ffffffffffff // 2.4999999999999996
4004000000000000 // 2.5
4004000000000001 // 2.5000000000000004

@bukka

bukka commented Sep 15, 2023

Copy link
Copy Markdown
Member

Doesn't this have the same effect as #12162 ? As I understand it the last part of condition is pretty much fallback to simple comparison check. Or will the previous mode specific checks have any sensible impact when used in pre-rounding?

I assume that the additional check is only useful in pre-rounding, right? If so, it might be a slight waste to do it all the time.

@SakiTakamachi

SakiTakamachi commented Sep 15, 2023

Copy link
Copy Markdown
Member

@bukka
This fixes improper rounding due to floating point precision issues.

For example, it is wrong to round 0.49999999999999994 to 1.
However, the existing processing looks like this:

0.49999999999999994 + 0.5
= 0.99999999999999994 // this is 3ff0000000000000 in IEEE754 with double precision.
= 1

Double precision makes it impossible to distinguish between 0.99999999999999994 and 1, so it is incorrectly rounded to 1.


Also, 0.49999999999999994 is a number that is not affected by #12162, which is a completely different problem.

@bukka

Copy link
Copy Markdown
Member

Ah ok I see now. It makes sense

TimWolla added a commit to TimWolla/php-src that referenced this pull request Sep 15, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see phpGH-12143 (this fixes one of the reported cases)
ClosesphpGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
TimWolla added a commit that referenced this pull request Sep 19, 2023
This change makes the implementation much easier to understand, by explicitly
handling the various cases.
It fixes rounding for `0.49999999999999994`, because no loss of precision
happens by adding / subtracing `0.5` before turning the result into an integral
float. Instead the fractional parts are explicitly compared.
see GH-12143 (this fixes one of the reported cases)
ClosesGH-12159 which was an alternative attempt to fix the rounding issue for
`0.49999999999999994`
@jorgsowa
jorgsowa deleted the fix-gh-12143 branch July 10, 2026 14:30
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incorrect round($num, 0, PHP_ROUND_HALF_UP) result for $num = 1.4999999999999998 / 4503599627370495.5

4 participants

@jorgsowa@SakiTakamachi@TimWolla@bukka