From 9801f2cc03786e3aa1ece68730e35b4e950f8edf Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Fri, 22 Jan 2021 17:24:18 -0500 Subject: [PATCH 1/2] Fix #76093: Format strings w/o loss of precision w/ FORMAT_TYPE_DECIMAL Passing the argument to NumberFormat::format() as a number loses precision if the value can not be represented precisely as a double or long integer. The icu library provides a "decimal number" type that avoids the loss of prevision when the value is passed as a string. Add a new FORMAT_TYPE_DECIMAL to explicitly request the argument be converted to a string and then passed to icu that way. --- Zend/zend_API.h | 25 +++++++++ ext/intl/formatter/formatter.stub.php | 7 ++- ext/intl/formatter/formatter_arginfo.h | 10 +++- ext/intl/formatter/formatter_format.c | 61 +++++++++++++++++++--- ext/intl/formatter/formatter_format.h | 1 + ext/intl/php_intl.stub.php | 2 +- ext/intl/php_intl_arginfo.h | 4 +- ext/intl/tests/bug48227.phpt | 38 ++++++++++++-- ext/intl/tests/bug76093.phpt | 71 ++++++++++++++++++++++++++ 9 files changed, 203 insertions(+), 16 deletions(-) create mode 100644 ext/intl/tests/bug76093.phpt diff --git a/Zend/zend_API.h b/Zend/zend_API.h index 1f9f1f1201984..fdbd0ae7f7afe 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -1488,6 +1488,8 @@ static zend_always_inline zval *zend_try_array_init(zval *zv) _(Z_EXPECTED_ARRAY_OR_STRING_OR_NULL, "of type array|string|null") \ _(Z_EXPECTED_STRING_OR_LONG, "of type string|int") \ _(Z_EXPECTED_STRING_OR_LONG_OR_NULL, "of type string|int|null") \ + _(Z_EXPECTED_STRING_OR_NUMBER, "of type string|int|float") \ + _(Z_EXPECTED_STRING_OR_NUMBER_OR_NULL, "of type string|int|float|null") \ _(Z_EXPECTED_OBJECT_OR_CLASS_NAME, "an object or a valid class name") \ _(Z_EXPECTED_OBJECT_OR_CLASS_NAME_OR_NULL, "an object, a valid class name, or null") \ _(Z_EXPECTED_OBJECT_OR_STRING, "of type object|string") \ @@ -2095,6 +2097,17 @@ ZEND_API ZEND_COLD void zend_argument_value_error(uint32_t arg_num, const char * #define Z_PARAM_STR_OR_LONG_OR_NULL(dest_str, dest_long, is_null) \ Z_PARAM_STR_OR_LONG_EX(dest_str, dest_long, is_null, 1); +#define Z_PARAM_STR_OR_NUMBER_EX(dest, check_null) \ + Z_PARAM_PROLOGUE(0, 0); \ + if (UNEXPECTED(!zend_parse_arg_str_or_number(_arg, &dest, check_null, _i))) { \ + _expected_type = check_null ? Z_EXPECTED_STRING_OR_NUMBER_OR_NULL : Z_EXPECTED_STRING_OR_NUMBER; \ + _error_code = ZPP_ERROR_WRONG_ARG; \ + break; \ + } + +#define Z_PARAM_STR_OR_NUMBER(dest) \ + Z_PARAM_STR_OR_NUMBER_EX(dest, 0) + /* End of new parameter parsing API */ /* Inlined implementations shared by new and old parameter parsing APIs */ @@ -2431,6 +2444,18 @@ static zend_always_inline bool zend_parse_arg_str_or_long(zval *arg, zend_string return 1; } +static zend_always_inline bool zend_parse_arg_str_or_number(zval *arg, zval **dest, bool check_null, uint32_t arg_num) +{ + if (EXPECTED(Z_TYPE_P(arg) == IS_LONG || Z_TYPE_P(arg) == IS_DOUBLE || Z_TYPE_P(arg) == IS_STRING)) { + *dest = arg; + } else if (check_null && EXPECTED(Z_TYPE_P(arg) == IS_NULL)) { + *dest = NULL; + } else { + return zend_parse_arg_number_slow(arg, dest, arg_num); + } + return 1; +} + static zend_always_inline bool zend_parse_arg_obj_or_class_name( zval *arg, zend_class_entry **destination, bool allow_null ) { diff --git a/ext/intl/formatter/formatter.stub.php b/ext/intl/formatter/formatter.stub.php index da6a6e3c5a6c2..378269869f70f 100644 --- a/ext/intl/formatter/formatter.stub.php +++ b/ext/intl/formatter/formatter.stub.php @@ -390,6 +390,11 @@ class NumberFormatter * @cvalue FORMAT_TYPE_DOUBLE */ public const TYPE_DOUBLE = UNKNOWN; + /** + * @var int + * @cvalue FORMAT_TYPE_DECIMAL + */ + public const TYPE_DECIMAL = UNKNOWN; /** * @var int * @cvalue FORMAT_TYPE_CURRENCY @@ -408,7 +413,7 @@ public static function create(string $locale, int $style, ?string $pattern = nul * @tentative-return-type * @alias numfmt_format */ - public function format(int|float $num, int $type = NumberFormatter::TYPE_DEFAULT): string|false {} + public function format(int|float|string $num, int $type = NumberFormatter::TYPE_DEFAULT): string|false {} /** * @param int $offset diff --git a/ext/intl/formatter/formatter_arginfo.h b/ext/intl/formatter/formatter_arginfo.h index 3488b025c725c..412c42aa741d8 100644 --- a/ext/intl/formatter/formatter_arginfo.h +++ b/ext/intl/formatter/formatter_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: de64db0e66d2113dd10c556cd61324a7a7952973 */ + * Stub hash: f975c515ccdbc6d6df579b760d64110fc3ce05c0 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_NumberFormatter___construct, 0, 0, 2) ZEND_ARG_TYPE_INFO(0, locale, IS_STRING, 0) @@ -14,7 +14,7 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_OBJ_INFO_EX(arginfo_class_NumberFormatter_c ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_MASK_EX(arginfo_class_NumberFormatter_format, 0, 1, MAY_BE_STRING|MAY_BE_FALSE) - ZEND_ARG_TYPE_MASK(0, num, MAY_BE_LONG|MAY_BE_DOUBLE, NULL) + ZEND_ARG_TYPE_MASK(0, num, MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING, NULL) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, type, IS_LONG, 0, "NumberFormatter::TYPE_DEFAULT") ZEND_END_ARG_INFO() @@ -568,6 +568,12 @@ static zend_class_entry *register_class_NumberFormatter(void) zend_declare_class_constant_ex(class_entry, const_TYPE_DOUBLE_name, &const_TYPE_DOUBLE_value, ZEND_ACC_PUBLIC, NULL); zend_string_release(const_TYPE_DOUBLE_name); + zval const_TYPE_DECIMAL_value; + ZVAL_LONG(&const_TYPE_DECIMAL_value, FORMAT_TYPE_DECIMAL); + zend_string *const_TYPE_DECIMAL_name = zend_string_init_interned("TYPE_DECIMAL", sizeof("TYPE_DECIMAL") - 1, 1); + zend_declare_class_constant_ex(class_entry, const_TYPE_DECIMAL_name, &const_TYPE_DECIMAL_value, ZEND_ACC_PUBLIC, NULL); + zend_string_release(const_TYPE_DECIMAL_name); + zval const_TYPE_CURRENCY_value; ZVAL_LONG(&const_TYPE_CURRENCY_value, FORMAT_TYPE_CURRENCY); zend_string *const_TYPE_CURRENCY_name = zend_string_init_interned("TYPE_CURRENCY", sizeof("TYPE_CURRENCY") - 1, 1); diff --git a/ext/intl/formatter/formatter_format.c b/ext/intl/formatter/formatter_format.c index 1c9d5b5bd9103..9be3e63369466 100644 --- a/ext/intl/formatter/formatter_format.c +++ b/ext/intl/formatter/formatter_format.c @@ -34,11 +34,22 @@ PHP_FUNCTION( numfmt_format ) int32_t formatted_len = USIZE(format_buf); FORMATTER_METHOD_INIT_VARS; + object = getThis(); + /* Parse parameters. */ - if( zend_parse_method_parameters( ZEND_NUM_ARGS(), getThis(), "On|l", - &object, NumberFormatter_ce_ptr, &number, &type ) == FAILURE ) - { - RETURN_THROWS(); + if (object) { + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_STR_OR_NUMBER(number) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(type) + ZEND_PARSE_PARAMETERS_END(); + } else { + ZEND_PARSE_PARAMETERS_START(2, 3) + Z_PARAM_OBJECT_OF_CLASS(object, NumberFormatter_ce_ptr) + Z_PARAM_STR_OR_NUMBER(number) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(type) + ZEND_PARSE_PARAMETERS_END(); } /* Fetch the object. */ @@ -53,10 +64,23 @@ PHP_FUNCTION( numfmt_format ) case IS_DOUBLE: type = FORMAT_TYPE_DOUBLE; break; - EMPTY_SWITCH_DEFAULT_CASE(); + case IS_STRING: + 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)); + RETURN_THROWS(); } } + // 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: convert_to_long(number); @@ -76,7 +100,13 @@ PHP_FUNCTION( numfmt_format ) case FORMAT_TYPE_INT64: { - int64_t value = (Z_TYPE_P(number) == IS_DOUBLE)?(int64_t)Z_DVAL_P(number):Z_LVAL_P(number); + int64_t value; + if (Z_TYPE_P(number) == IS_DOUBLE) { + value = (int64_t)Z_DVAL_P(number); + } else { + convert_to_long(number); + value = Z_LVAL_P(number); + } 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 +133,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(); + } + // 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()) { const char *space; @@ -113,6 +161,7 @@ PHP_FUNCTION( numfmt_format ) zend_argument_value_error(3, "cannot be NumberFormatter::TYPE_CURRENCY constant, use numfmt_format_currency() function instead"); } RETURN_THROWS(); + default: zend_argument_value_error(getThis() ? 2 : 3, "must be a NumberFormatter::TYPE_* constant"); RETURN_THROWS(); diff --git a/ext/intl/formatter/formatter_format.h b/ext/intl/formatter/formatter_format.h index 0238d5d4b8b08..58a2cc63a40e1 100644 --- a/ext/intl/formatter/formatter_format.h +++ b/ext/intl/formatter/formatter_format.h @@ -22,5 +22,6 @@ #define FORMAT_TYPE_INT64 2 #define FORMAT_TYPE_DOUBLE 3 #define FORMAT_TYPE_CURRENCY 4 +#define FORMAT_TYPE_DECIMAL 5 #endif // FORMATTER_FORMAT_H diff --git a/ext/intl/php_intl.stub.php b/ext/intl/php_intl.stub.php index c21ad58fa092e..2370e513fd6a3 100644 --- a/ext/intl/php_intl.stub.php +++ b/ext/intl/php_intl.stub.php @@ -390,7 +390,7 @@ function datefmt_get_error_message(IntlDateFormatter $formatter): string {} function numfmt_create(string $locale, int $style, ?string $pattern = null): ?NumberFormatter {} -function numfmt_format(NumberFormatter $formatter, int|float $num, int $type = NumberFormatter::TYPE_DEFAULT): string|false {} +function numfmt_format(NumberFormatter $formatter, int|float|string $num, int $type = NumberFormatter::TYPE_DEFAULT): string|false {} /** @param int $offset */ function numfmt_parse(NumberFormatter $formatter, string $string, int $type = NumberFormatter::TYPE_DOUBLE, &$offset = null): int|float|false {} diff --git a/ext/intl/php_intl_arginfo.h b/ext/intl/php_intl_arginfo.h index b715413989145..791c69d90dc9b 100644 --- a/ext/intl/php_intl_arginfo.h +++ b/ext/intl/php_intl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 7008d442eba36e2bc468cc4a7a30eb859d10c07d */ + * Stub hash: eddb2b8e0feeba3d20fcb5d9b02571477d7e732b */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_intlcal_create_instance, 0, 0, IntlCalendar, 1) ZEND_ARG_INFO_WITH_DEFAULT_VALUE(0, timezone, "null") @@ -375,7 +375,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_numfmt_format, 0, 2, MAY_BE_STRING|MAY_BE_FALSE) ZEND_ARG_OBJ_INFO(0, formatter, NumberFormatter, 0) - ZEND_ARG_TYPE_MASK(0, num, MAY_BE_LONG|MAY_BE_DOUBLE, NULL) + ZEND_ARG_TYPE_MASK(0, num, MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING, NULL) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, type, IS_LONG, 0, "NumberFormatter::TYPE_DEFAULT") ZEND_END_ARG_INFO() diff --git a/ext/intl/tests/bug48227.phpt b/ext/intl/tests/bug48227.phpt index 8371a872eaee8..1f256858330ae 100644 --- a/ext/intl/tests/bug48227.phpt +++ b/ext/intl/tests/bug48227.phpt @@ -5,8 +5,11 @@ intl --FILE-- format($value)); } catch (TypeError $ex) { @@ -14,11 +17,38 @@ foreach (['', 1, NULL, $x] as $value) { } } +echo("\nNon-OOP\n"); +$x = numfmt_create('en_US', NumberFormatter::DECIMAL); +foreach (array_merge($testcases, [$x]) as $value) { + try { + var_dump(numfmt_format($x, $value)); + } catch (TypeError $ex) { + echo $ex->getMessage(), PHP_EOL; + } +} + ?> --EXPECTF-- -NumberFormatter::format(): Argument #1 ($num) must be of type int|float, string given +OOP +bool(false) string(1) "1" -Deprecated: NumberFormatter::format(): Passing null to parameter #1 ($num) of type int|float is deprecated in %s on line %d +Deprecated: NumberFormatter::format(): Passing null to parameter #1 ($num) of type string|int|float is deprecated in %s on line %d +string(1) "0" +string(1) "1" +string(1) "0" +NumberFormatter::format(): Argument #1 ($num) must be of type string|int|float, array given +NumberFormatter::format(): Argument #1 ($num) must be of type string|int|float, stdClass given +NumberFormatter::format(): Argument #1 ($num) must be of type string|int|float, NumberFormatter given + +Non-OOP +bool(false) +string(1) "1" + +Deprecated: numfmt_format(): Passing null to parameter #2 ($num) of type string|int|float is deprecated in %s on line %d +string(1) "0" +string(1) "1" string(1) "0" -NumberFormatter::format(): Argument #1 ($num) must be of type int|float, NumberFormatter given +numfmt_format(): Argument #2 ($num) must be of type string|int|float, array given +numfmt_format(): Argument #2 ($num) must be of type string|int|float, stdClass given +numfmt_format(): Argument #2 ($num) must be of type string|int|float, NumberFormatter given diff --git a/ext/intl/tests/bug76093.phpt b/ext/intl/tests/bug76093.phpt new file mode 100644 index 0000000000000..2ed5e5791d942 --- /dev/null +++ b/ext/intl/tests/bug76093.phpt @@ -0,0 +1,71 @@ +--TEST-- +Bug #76093 (NumberFormatter::format loses precision) +--SKIPIF-- + +--FILE-- + $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; + } +} + +?> +--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" +} +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" +} +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" +} From b7d6cd678160848a1f5d866c0e0a93db38dfd25f Mon Sep 17 00:00:00 2001 From: "C. Scott Ananian" Date: Sat, 23 Jan 2021 20:06:41 -0500 Subject: [PATCH 2/2] Fix #76093, part 2: Format currency strings w/o loss of precision This extends the argument types of NumberFormat::formatCurrency() to include string, in the same way the earlier patch extended NumberFormat::format() to allow a string. This allows formatting values which can't be represented precisely as a double or long integer, using the "decimal number" type of the icu library. --- ext/intl/formatter/formatter.stub.php | 2 +- ext/intl/formatter/formatter_arginfo.h | 4 +-- ext/intl/formatter/formatter_format.c | 34 ++++++++++++++++++++------ ext/intl/php_intl.stub.php | 2 +- ext/intl/php_intl_arginfo.h | 4 +-- ext/intl/tests/bug76093.phpt | 16 +++++++++--- ext/intl/tests/bug78912.phpt | 7 ++++++ 7 files changed, 53 insertions(+), 16 deletions(-) diff --git a/ext/intl/formatter/formatter.stub.php b/ext/intl/formatter/formatter.stub.php index 378269869f70f..816765e15e8e5 100644 --- a/ext/intl/formatter/formatter.stub.php +++ b/ext/intl/formatter/formatter.stub.php @@ -426,7 +426,7 @@ public function parse(string $string, int $type = NumberFormatter::TYPE_DOUBLE, * @tentative-return-type * @alias numfmt_format_currency */ - public function formatCurrency(float $amount, string $currency): string|false {} + public function formatCurrency(int|float|string $amount, string $currency): string|false {} /** * @param string $currency diff --git a/ext/intl/formatter/formatter_arginfo.h b/ext/intl/formatter/formatter_arginfo.h index 412c42aa741d8..7a5a45b1ee5a7 100644 --- a/ext/intl/formatter/formatter_arginfo.h +++ b/ext/intl/formatter/formatter_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: f975c515ccdbc6d6df579b760d64110fc3ce05c0 */ + * Stub hash: b8ef26728a8fb9b942867d803336ad1c63147cb7 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_NumberFormatter___construct, 0, 0, 2) ZEND_ARG_TYPE_INFO(0, locale, IS_STRING, 0) @@ -25,7 +25,7 @@ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_MASK_EX(arginfo_class_NumberFormatter_ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_MASK_EX(arginfo_class_NumberFormatter_formatCurrency, 0, 2, MAY_BE_STRING|MAY_BE_FALSE) - ZEND_ARG_TYPE_INFO(0, amount, IS_DOUBLE, 0) + ZEND_ARG_TYPE_MASK(0, amount, MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING, NULL) ZEND_ARG_TYPE_INFO(0, currency, IS_STRING, 0) ZEND_END_ARG_INFO() diff --git a/ext/intl/formatter/formatter_format.c b/ext/intl/formatter/formatter_format.c index 9be3e63369466..74a34bb87576a 100644 --- a/ext/intl/formatter/formatter_format.c +++ b/ext/intl/formatter/formatter_format.c @@ -174,7 +174,7 @@ PHP_FUNCTION( numfmt_format ) /* {{{ Format a number as currency. */ PHP_FUNCTION( numfmt_format_currency ) { - double number; + zval *number; UChar format_buf[32]; UChar* formatted = format_buf; int32_t formatted_len = USIZE(format_buf); @@ -184,11 +184,20 @@ PHP_FUNCTION( numfmt_format_currency ) int32_t scurrency_len = 0; FORMATTER_METHOD_INIT_VARS; + object = getThis(); + /* Parse parameters. */ - if( zend_parse_method_parameters( ZEND_NUM_ARGS(), getThis(), "Ods", - &object, NumberFormatter_ce_ptr, &number, ¤cy, ¤cy_len ) == FAILURE ) - { - RETURN_THROWS(); + if (object) { + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR_OR_NUMBER(number) + Z_PARAM_STRING(currency, currency_len) + ZEND_PARSE_PARAMETERS_END(); + } else { + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_OBJECT_OF_CLASS(object, NumberFormatter_ce_ptr) + Z_PARAM_STR_OR_NUMBER(number) + Z_PARAM_STRING(currency, currency_len) + ZEND_PARSE_PARAMETERS_END(); } /* Fetch the object. */ @@ -197,9 +206,16 @@ PHP_FUNCTION( numfmt_format_currency ) /* Convert currency to UTF-16. */ intl_convert_utf8_to_utf16(&scurrency, &scurrency_len, currency, currency_len, &INTL_DATA_ERROR_CODE(nfo)); INTL_METHOD_CHECK_STATUS( nfo, "Currency conversion to UTF-16 failed" ); + unum_setTextAttribute(FORMATTER_OBJECT(nfo), UNUM_CURRENCY_CODE, scurrency, scurrency_len, &INTL_DATA_ERROR_CODE(nfo)); + INTL_METHOD_CHECK_STATUS( nfo, "Setting currency code failed" ); /* Format the number using a fixed-length buffer. */ - formatted_len = unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), number, scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo)); + if (Z_TYPE_P(number) == IS_STRING) { + formatted_len = unum_formatDecimal(FORMATTER_OBJECT(nfo), Z_STRVAL_P(number), Z_STRLEN_P(number), formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo)); + } else { + convert_to_double(number); + formatted_len = unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), Z_DVAL_P(number), scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo)); + } /* If the buffer turned out to be too small * then allocate another buffer dynamically @@ -208,7 +224,11 @@ PHP_FUNCTION( numfmt_format_currency ) if (INTL_DATA_ERROR_CODE(nfo) == U_BUFFER_OVERFLOW_ERROR) { intl_error_reset(INTL_DATA_ERROR_P(nfo)); formatted = eumalloc(formatted_len); - unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), number, scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo)); + if (Z_TYPE_P(number) == IS_STRING) { + unum_formatDecimal(FORMATTER_OBJECT(nfo), Z_STRVAL_P(number), Z_STRLEN_P(number), formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo)); + } else { + unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), Z_DVAL_P(number), scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo)); + } } if( U_FAILURE( INTL_DATA_ERROR_CODE((nfo)) ) ) { diff --git a/ext/intl/php_intl.stub.php b/ext/intl/php_intl.stub.php index 2370e513fd6a3..96d8e7d7d1f2e 100644 --- a/ext/intl/php_intl.stub.php +++ b/ext/intl/php_intl.stub.php @@ -395,7 +395,7 @@ function numfmt_format(NumberFormatter $formatter, int|float|string $num, int $t /** @param int $offset */ function numfmt_parse(NumberFormatter $formatter, string $string, int $type = NumberFormatter::TYPE_DOUBLE, &$offset = null): int|float|false {} -function numfmt_format_currency(NumberFormatter $formatter, float $amount, string $currency): string|false {} +function numfmt_format_currency(NumberFormatter $formatter, int|float|string $amount, string $currency): string|false {} /** * @param string $currency diff --git a/ext/intl/php_intl_arginfo.h b/ext/intl/php_intl_arginfo.h index 791c69d90dc9b..015a9b0c37999 100644 --- a/ext/intl/php_intl_arginfo.h +++ b/ext/intl/php_intl_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: eddb2b8e0feeba3d20fcb5d9b02571477d7e732b */ + * Stub hash: 768ef5d279f924e57a93af40422ebb5acb46c47e */ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_intlcal_create_instance, 0, 0, IntlCalendar, 1) ZEND_ARG_INFO_WITH_DEFAULT_VALUE(0, timezone, "null") @@ -388,7 +388,7 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_numfmt_format_currency, 0, 3, MAY_BE_STRING|MAY_BE_FALSE) ZEND_ARG_OBJ_INFO(0, formatter, NumberFormatter, 0) - ZEND_ARG_TYPE_INFO(0, amount, IS_DOUBLE, 0) + ZEND_ARG_TYPE_MASK(0, amount, MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING, NULL) ZEND_ARG_TYPE_INFO(0, currency, IS_STRING, 0) ZEND_END_ARG_INFO() diff --git a/ext/intl/tests/bug76093.phpt b/ext/intl/tests/bug76093.phpt index 2ed5e5791d942..306f7cb0ff14d 100644 --- a/ext/intl/tests/bug76093.phpt +++ b/ext/intl/tests/bug76093.phpt @@ -7,6 +7,7 @@ Bug #76093 (NumberFormatter::format loses precision) # See also https://phabricator.wikimedia.org/T268456 $x = new NumberFormatter('en_US', NumberFormatter::DECIMAL); +$x2 = new NumberFormatter('en_US', NumberFormatter::CURRENCY); foreach ([ '999999999999999999', # Fits in signed 64-bit integer '9999999999999999999', # Does not fit in signed 64-bit integer @@ -25,6 +26,9 @@ foreach ([ 'int64' => $x->format($value, NumberFormatter::TYPE_INT64), 'double' => $x->format($value, NumberFormatter::TYPE_DOUBLE), 'decimal' => $x->format($value, NumberFormatter::TYPE_DECIMAL), + # formatCurrency requires the NumberFormatter to be created with + # the CURRENCY or CURRENCY_ACCOUNTING style. + 'currency' => $x2->formatCurrency($value, 'USD'), ]); } catch (TypeError $ex) { echo $ex->getMessage(), PHP_EOL; @@ -33,7 +37,7 @@ foreach ([ ?> --EXPECTF-- -array(5) { +array(6) { ["input"]=> string(18) "999999999999999999" ["default"]=> @@ -44,8 +48,10 @@ array(5) { string(25) "1,000,000,000,000,000,000" ["decimal"]=> string(23) "999,999,999,999,999,999" + ["currency"]=> + string(27) "$999,999,999,999,999,999.00" } -array(5) { +array(6) { ["input"]=> string(19) "9999999999999999999" ["default"]=> @@ -56,8 +62,10 @@ array(5) { string(26) "10,000,000,000,000,000,000" ["decimal"]=> string(25) "9,999,999,999,999,999,999" + ["currency"]=> + string(29) "$9,999,999,999,999,999,999.00" } -array(5) { +array(6) { ["input"]=> float(1.0E+19) ["default"]=> @@ -68,4 +76,6 @@ array(5) { string(26) "10,000,000,000,000,000,000" ["decimal"]=> string(26) "10,000,000,000,000,000,000" + ["currency"]=> + string(30) "$10,000,000,000,000,000,000.00" } diff --git a/ext/intl/tests/bug78912.phpt b/ext/intl/tests/bug78912.phpt index 2e36c6007e4fa..5dfbf5df553fd 100644 --- a/ext/intl/tests/bug78912.phpt +++ b/ext/intl/tests/bug78912.phpt @@ -10,6 +10,13 @@ if (version_compare(INTL_ICU_VERSION, '53.0') < 0) die('skip for ICU >= 53.0'); formatCurrency(-12345.67, 'USD')); +# Also some tests for bug #76093 while we're at it. +var_dump($nf->formatCurrency(9999999999999999999, 'USD')); # gets rounded +var_dump($nf->formatCurrency('-12345.67', 'USD')); +var_dump($nf->formatCurrency('9999999999999999999', 'USD')); # not rounded! ?> --EXPECT-- string(12) "($12,345.67)" +string(30) "$10,000,000,000,000,000,000.00" +string(12) "($12,345.67)" +string(29) "$9,999,999,999,999,999,999.00"