-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Fix #76093: Format strings w/o loss of precision w/ FORMAT_TYPE_DECIMAL #12432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -35,7 +35,7 @@ PHP_FUNCTION( numfmt_format ) | |
FORMATTER_METHOD_INIT_VARS; | ||
|
||
/* Parse parameters. */ | ||
if( zend_parse_method_parameters( ZEND_NUM_ARGS(), getThis(), "On|l", | ||
if( zend_parse_method_parameters( ZEND_NUM_ARGS(), getThis(), "Oz|l", | ||
&object, NumberFormatter_ce_ptr, &number, &type ) == FAILURE ) | ||
{ | ||
RETURN_THROWS(); | ||
|
@@ -53,30 +53,65 @@ PHP_FUNCTION( numfmt_format ) | |
case IS_DOUBLE: | ||
type = FORMAT_TYPE_DOUBLE; | ||
break; | ||
EMPTY_SWITCH_DEFAULT_CASE(); | ||
case IS_STRING: | ||
type = FORMAT_TYPE_DECIMAL; | ||
break; | ||
case IS_OBJECT: | ||
type = FORMAT_TYPE_DECIMAL; | ||
break; | ||
default: | ||
zend_argument_type_error(1, "must be of type int|float|string, %s given", zend_zval_type_name(number)); | ||
lucaswerkmeister marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
// Avoid losing precision on 32-bit platforms where PHP's "long" isn't | ||
// as long as the FORMAT_TYPE_INT64 which is requested. | ||
#if SIZEOF_ZEND_LONG < 8 | ||
if (Z_TYPE_P(number) == IS_STRING && type == FORMAT_TYPE_INT64) { | ||
type = FORMAT_TYPE_DECIMAL; | ||
} | ||
#endif | ||
|
||
switch(type) { | ||
case FORMAT_TYPE_INT32: | ||
{ | ||
bool failed = true; | ||
int64_t value_64 = zval_try_get_long(number, &failed); | ||
if (failed) { | ||
zend_argument_type_error(getThis() ? 1 : 2, | ||
"must be of type int when argument #%d ($format) is NumberFormatter::TYPE_INT32", getThis() ? 2 : 3); | ||
RETURN_THROWS(); | ||
} | ||
if (value_64 < -2147483648 || value_64 > 2147483647) { | ||
zend_argument_value_error(getThis() ? 1 : 2, | ||
"must fit in 32 bits when argument #%d ($format) is NumberFormatter::TYPE_INT32", getThis() ? 2 : 3); | ||
RETURN_THROWS(); | ||
} | ||
convert_to_long(number); | ||
formatted_len = unum_format(FORMATTER_OBJECT(nfo), (int32_t)Z_LVAL_P(number), | ||
formatted_len = unum_format(FORMATTER_OBJECT(nfo), (int32_t)value_64, | ||
formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo)); | ||
if (INTL_DATA_ERROR_CODE(nfo) == U_BUFFER_OVERFLOW_ERROR) { | ||
intl_error_reset(INTL_DATA_ERROR_P(nfo)); | ||
formatted = eumalloc(formatted_len); | ||
formatted_len = unum_format(FORMATTER_OBJECT(nfo), (int32_t)Z_LVAL_P(number), | ||
formatted_len = unum_format(FORMATTER_OBJECT(nfo), (int32_t)value_64, | ||
formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo)); | ||
if (U_FAILURE( INTL_DATA_ERROR_CODE(nfo) ) ) { | ||
efree(formatted); | ||
} | ||
} | ||
INTL_METHOD_CHECK_STATUS( nfo, "Number formatting failed" ); | ||
} | ||
break; | ||
|
||
case FORMAT_TYPE_INT64: | ||
{ | ||
int64_t value = (Z_TYPE_P(number) == IS_DOUBLE)?(int64_t)Z_DVAL_P(number):Z_LVAL_P(number); | ||
bool failed = true; | ||
int64_t value = zval_try_get_long(number, &failed); | ||
if (failed) { | ||
zend_argument_type_error(getThis() ? 1 : 2, | ||
"must be of type int when argument #%d ($format) is NumberFormatter::TYPE_INT64", getThis() ? 2 : 3); | ||
RETURN_THROWS(); | ||
} | ||
formatted_len = unum_formatInt64(FORMATTER_OBJECT(nfo), value, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo)); | ||
if (INTL_DATA_ERROR_CODE(nfo) == U_BUFFER_OVERFLOW_ERROR) { | ||
intl_error_reset(INTL_DATA_ERROR_P(nfo)); | ||
|
@@ -103,6 +138,24 @@ PHP_FUNCTION( numfmt_format ) | |
} | ||
INTL_METHOD_CHECK_STATUS( nfo, "Number formatting failed" ); | ||
break; | ||
|
||
case FORMAT_TYPE_DECIMAL: | ||
if (!try_convert_to_string(number)) { | ||
RETURN_THROWS(); | ||
} | ||
Comment on lines
+143
to
+145
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This could be simplified as at this point the value should be |
||
// Convert string as a DecimalNumber, so we don't lose precision | ||
formatted_len = unum_formatDecimal(FORMATTER_OBJECT(nfo), Z_STRVAL_P(number), Z_STRLEN_P(number), formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo)); | ||
if (INTL_DATA_ERROR_CODE(nfo) == U_BUFFER_OVERFLOW_ERROR) { | ||
intl_error_reset(INTL_DATA_ERROR_P(nfo)); | ||
formatted = eumalloc(formatted_len); | ||
unum_formatDecimal(FORMATTER_OBJECT(nfo), Z_STRVAL_P(number), Z_STRLEN_P(number), formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo)); | ||
if (U_FAILURE( INTL_DATA_ERROR_CODE(nfo) ) ) { | ||
efree(formatted); | ||
} | ||
} | ||
INTL_METHOD_CHECK_STATUS( nfo, "Number formatting failed" ); | ||
break; | ||
|
||
case FORMAT_TYPE_CURRENCY: | ||
if (getThis()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aside Nit: this could now be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried it, but it caused several test failures (seemingly |
||
const char *space; | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
--TEST-- | ||
Bug #76093 (NumberFormatter::format loses precision) | ||
--EXTENSIONS-- | ||
intl | ||
--FILE-- | ||
<?php | ||
|
||
class Bug76093Stringable implements Stringable { | ||
public function __construct( private readonly string $string ) {} | ||
|
||
public function __toString(): string { | ||
return $this->string; | ||
} | ||
} | ||
|
||
# See also https://phabricator.wikimedia.org/T268456 | ||
$x = new NumberFormatter('en_US', NumberFormatter::DECIMAL); | ||
foreach ([ | ||
'999999999999999999', # Fits in signed 64-bit integer | ||
'9999999999999999999', # Does not fit in signed 64-bit integer | ||
9999999999999999999, # Precision loss seen when passing as number | ||
] as $value) { | ||
try { | ||
var_dump([ | ||
'input' => $value, | ||
'default' => $x->format($value), | ||
# Note that TYPE_INT64 isn't actually guaranteed to have an | ||
# 64-bit integer as input, because PHP on 32-bit platforms only | ||
# has 32-bit integers. If you pass the value as a string, PHP | ||
# will use the TYPE_DECIMAL type in order to extend the range. | ||
# Also, casting from double to int64 when the int64 range | ||
# is exceeded results in an implementation-defined value. | ||
'int64' => $x->format($value, NumberFormatter::TYPE_INT64), | ||
'double' => $x->format($value, NumberFormatter::TYPE_DOUBLE), | ||
'decimal' => $x->format($value, NumberFormatter::TYPE_DECIMAL), | ||
]); | ||
} catch (TypeError $ex) { | ||
echo $ex->getMessage(), PHP_EOL; | ||
} | ||
} | ||
|
||
# Stringable object also supported | ||
try { | ||
echo $x->format(new Bug76093Stringable('9999999999999999999')), PHP_EOL; | ||
} catch (TypeError $ex) { | ||
echo $ex->getMessage(), PHP_EOL; | ||
} | ||
|
||
?> | ||
--EXPECTF-- | ||
array(5) { | ||
["input"]=> | ||
string(18) "999999999999999999" | ||
["default"]=> | ||
string(23) "999,999,999,999,999,999" | ||
["int64"]=> | ||
string(23) "999,999,999,999,999,999" | ||
["double"]=> | ||
string(25) "1,000,000,000,000,000,000" | ||
["decimal"]=> | ||
string(23) "999,999,999,999,999,999" | ||
} | ||
|
||
Deprecated: Implicit conversion from float-string "9999999999999999999" to int loses precision in %s on line %d | ||
array(5) { | ||
["input"]=> | ||
string(19) "9999999999999999999" | ||
["default"]=> | ||
string(25) "9,999,999,999,999,999,999" | ||
["int64"]=> | ||
string(%d) "%r9,223,372,036,854,775,807|9,999,999,999,999,999,999%r" | ||
["double"]=> | ||
string(26) "10,000,000,000,000,000,000" | ||
["decimal"]=> | ||
string(25) "9,999,999,999,999,999,999" | ||
} | ||
|
||
Deprecated: Implicit conversion from float 1.0E+19 to int loses precision in %s on line %d | ||
array(5) { | ||
["input"]=> | ||
float(1.0E+19) | ||
["default"]=> | ||
string(26) "10,000,000,000,000,000,000" | ||
["int64"]=> | ||
string(%d) "%r[+-]?[0-9,]+%r" | ||
["double"]=> | ||
string(26) "10,000,000,000,000,000,000" | ||
["decimal"]=> | ||
string(26) "10,000,000,000,000,000,000" | ||
} | ||
9,999,999,999,999,999,999 |
Uh oh!
There was an error while loading. Please reload this page.