From 428dadbdf67177c41dee891149e5a6180ee98adc Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Sat, 30 Sep 2023 16:28:02 +0200 Subject: [PATCH 1/3] number_format: float within range of int can be cast to int before rounding to not loose precision --- ext/standard/math.c | 10 +++++++ .../math/number_format_basiclong_64bit.phpt | 30 +++++++++++++++++-- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/ext/standard/math.c b/ext/standard/math.c index 734ebaba07564..7a977710eec45 100644 --- a/ext/standard/math.c +++ b/ext/standard/math.c @@ -1328,6 +1328,16 @@ PHP_FUNCTION(number_format) break; case IS_DOUBLE: + // On 64bit a given float within range of int but greater than 2^53 can be cast to int + // to not loose precision + if (UNEXPECTED( + (Z_DVAL_P(num) > 9007199254740992.0 || Z_DVAL_P(num) < -9007199254740992.0) + && Z_DVAL_P(num) < ZEND_LONG_MAX && Z_DVAL_P(num) > ZEND_LONG_MIN + )) { + RETURN_STR(_php_math_number_format_long((zend_long)Z_DVAL_P(num), dec, dec_point, dec_point_len, thousand_sep, thousand_sep_len)); + break; + } + if (dec >= 0) { dec_int = ZEND_LONG_INT_OVFL(dec) ? INT_MAX : (int)dec; } else { diff --git a/ext/standard/tests/math/number_format_basiclong_64bit.phpt b/ext/standard/tests/math/number_format_basiclong_64bit.phpt index 2f63091abb8c6..d4ff181342683 100644 --- a/ext/standard/tests/math/number_format_basiclong_64bit.phpt +++ b/ext/standard/tests/math/number_format_basiclong_64bit.phpt @@ -12,10 +12,12 @@ define("MAX_32Bit", 2147483647); define("MIN_64Bit", -9223372036854775807 - 1); define("MIN_32Bit", -2147483647 - 1); -$longVals = array( +$numbers = array( MAX_64Bit, MIN_64Bit, MAX_32Bit, MIN_32Bit, MAX_64Bit - MAX_32Bit, MIN_64Bit - MIN_32Bit, MAX_32Bit + 1, MIN_32Bit - 1, MAX_32Bit * 2, (MAX_32Bit * 2) + 1, (MAX_32Bit * 2) - 1, - MAX_64Bit -1, MAX_64Bit + 1, MIN_64Bit + 1, MIN_64Bit - 1 + MAX_64Bit - 1, MAX_64Bit + 1, MIN_64Bit + 1, MIN_64Bit - 1, + // floats rounded as int + 9223372036854774784.0, -9223372036854774784.0 ); $precisions = array( @@ -31,7 +33,7 @@ $precisions = array( PHP_INT_MIN, ); -foreach ($longVals as $longVal) { +foreach ($numbers as $longVal) { echo "--- testing: "; var_dump($longVal); foreach ($precisions as $precision) { @@ -207,3 +209,25 @@ foreach ($longVals as $longVal) { ... with precision -19: string(27) "-10,000,000,000,000,000,000" ... with precision -20: string(1) "0" ... with precision -9223372036854775808: string(1) "0" +--- testing: float(9.223372036854775E+18) +... with precision 5: string(31) "9,223,372,036,854,774,784.00000" +... with precision 0: string(25) "9,223,372,036,854,774,784" +... with precision -1: string(25) "9,223,372,036,854,774,780" +... with precision -5: string(25) "9,223,372,036,854,800,000" +... with precision -10: string(25) "9,223,372,040,000,000,000" +... with precision -11: string(25) "9,223,372,000,000,000,000" +... with precision -17: string(25) "9,200,000,000,000,000,000" +... with precision -19: string(26) "10,000,000,000,000,000,000" +... with precision -20: string(1) "0" +... with precision -9223372036854775808: string(1) "0" +--- testing: float(-9.223372036854775E+18) +... with precision 5: string(32) "-9,223,372,036,854,774,784.00000" +... with precision 0: string(26) "-9,223,372,036,854,774,784" +... with precision -1: string(26) "-9,223,372,036,854,774,780" +... with precision -5: string(26) "-9,223,372,036,854,800,000" +... with precision -10: string(26) "-9,223,372,040,000,000,000" +... with precision -11: string(26) "-9,223,372,000,000,000,000" +... with precision -17: string(26) "-9,200,000,000,000,000,000" +... with precision -19: string(27) "-10,000,000,000,000,000,000" +... with precision -20: string(1) "0" +... with precision -9223372036854775808: string(1) "0" From 36a259735d5990385be15c9582c244e41714327f Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Sat, 30 Sep 2023 19:02:00 +0200 Subject: [PATCH 2/3] Fixed double value within long check --- ext/standard/math.c | 2 +- ext/standard/tests/math/number_format_basiclong_64bit.phpt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ext/standard/math.c b/ext/standard/math.c index 7a977710eec45..1101d6ca5ecfb 100644 --- a/ext/standard/math.c +++ b/ext/standard/math.c @@ -1332,7 +1332,7 @@ PHP_FUNCTION(number_format) // to not loose precision if (UNEXPECTED( (Z_DVAL_P(num) > 9007199254740992.0 || Z_DVAL_P(num) < -9007199254740992.0) - && Z_DVAL_P(num) < ZEND_LONG_MAX && Z_DVAL_P(num) > ZEND_LONG_MIN + && ZEND_DOUBLE_FITS_LONG(Z_DVAL_P(num)) )) { RETURN_STR(_php_math_number_format_long((zend_long)Z_DVAL_P(num), dec, dec_point, dec_point_len, thousand_sep, thousand_sep_len)); break; diff --git a/ext/standard/tests/math/number_format_basiclong_64bit.phpt b/ext/standard/tests/math/number_format_basiclong_64bit.phpt index d4ff181342683..fb779e438f788 100644 --- a/ext/standard/tests/math/number_format_basiclong_64bit.phpt +++ b/ext/standard/tests/math/number_format_basiclong_64bit.phpt @@ -201,8 +201,8 @@ foreach ($numbers as $longVal) { --- testing: float(-9.223372036854776E+18) ... with precision 5: string(32) "-9,223,372,036,854,775,808.00000" ... with precision 0: string(26) "-9,223,372,036,854,775,808" -... with precision -1: string(26) "-9,223,372,036,854,775,808" -... with precision -5: string(26) "-9,223,372,036,854,800,384" +... with precision -1: string(26) "-9,223,372,036,854,775,810" +... with precision -5: string(26) "-9,223,372,036,854,800,000" ... with precision -10: string(26) "-9,223,372,040,000,000,000" ... with precision -11: string(26) "-9,223,372,000,000,000,000" ... with precision -17: string(26) "-9,200,000,000,000,000,000" From e977892eb02065ba0effeff55cadbe1ada8e54fc Mon Sep 17 00:00:00 2001 From: Marc Bennewitz Date: Mon, 2 Oct 2023 08:08:27 +0200 Subject: [PATCH 3/3] cast on 2^52 --- ext/standard/math.c | 6 +++--- .../tests/math/number_format_basiclong_64bit.phpt | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ext/standard/math.c b/ext/standard/math.c index 1101d6ca5ecfb..3a1c69cd97bff 100644 --- a/ext/standard/math.c +++ b/ext/standard/math.c @@ -1328,10 +1328,10 @@ PHP_FUNCTION(number_format) break; case IS_DOUBLE: - // On 64bit a given float within range of int but greater than 2^53 can be cast to int - // to not loose precision + // double values of >= 2^52 can not have fractional digits anymore + // Casting to long on 64bit will not loose precision on rounding if (UNEXPECTED( - (Z_DVAL_P(num) > 9007199254740992.0 || Z_DVAL_P(num) < -9007199254740992.0) + (Z_DVAL_P(num) >= 4503599627370496.0 || Z_DVAL_P(num) <= -4503599627370496.0) && ZEND_DOUBLE_FITS_LONG(Z_DVAL_P(num)) )) { RETURN_STR(_php_math_number_format_long((zend_long)Z_DVAL_P(num), dec, dec_point, dec_point_len, thousand_sep, thousand_sep_len)); diff --git a/ext/standard/tests/math/number_format_basiclong_64bit.phpt b/ext/standard/tests/math/number_format_basiclong_64bit.phpt index fb779e438f788..fd709fc648f92 100644 --- a/ext/standard/tests/math/number_format_basiclong_64bit.phpt +++ b/ext/standard/tests/math/number_format_basiclong_64bit.phpt @@ -17,7 +17,7 @@ $numbers = array( MAX_32Bit + 1, MIN_32Bit - 1, MAX_32Bit * 2, (MAX_32Bit * 2) + 1, (MAX_32Bit * 2) - 1, MAX_64Bit - 1, MAX_64Bit + 1, MIN_64Bit + 1, MIN_64Bit - 1, // floats rounded as int - 9223372036854774784.0, -9223372036854774784.0 + MAX_64Bit - 1024.0, MIN_64Bit + 1024.0 ); $precisions = array( @@ -33,12 +33,12 @@ $precisions = array( PHP_INT_MIN, ); -foreach ($numbers as $longVal) { +foreach ($numbers as $number) { echo "--- testing: "; - var_dump($longVal); + var_dump($number); foreach ($precisions as $precision) { echo "... with precision " . $precision . ": "; - var_dump(number_format($longVal, $precision)); + var_dump(number_format($number, $precision)); } }