Skip to content

Commit d1afe1f

Browse files
committed
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.
1 parent 975a285 commit d1afe1f

File tree

7 files changed

+52
-15
lines changed

7 files changed

+52
-15
lines changed

ext/intl/formatter/formatter.stub.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function parse(string $string, int $type = NumberFormatter::TYPE_DOUBLE,
3030
* @tentative-return-type
3131
* @alias numfmt_format_currency
3232
*/
33-
public function formatCurrency(float $amount, string $currency): string|false {}
33+
public function formatCurrency(int|float|string $amount, string $currency): string|false {}
3434

3535
/**
3636
* @param string $currency

ext/intl/formatter/formatter_arginfo.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: a3dc0e6258a22b5e27dcdd550715741b0c9b33d0 */
2+
* Stub hash: 56694496058da02f52daf26a8f1588f604791f12 */
33

44
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_NumberFormatter___construct, 0, 0, 2)
55
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_
2525
ZEND_END_ARG_INFO()
2626

2727
ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_MASK_EX(arginfo_class_NumberFormatter_formatCurrency, 0, 2, MAY_BE_STRING|MAY_BE_FALSE)
28-
ZEND_ARG_TYPE_INFO(0, amount, IS_DOUBLE, 0)
28+
ZEND_ARG_TYPE_MASK(0, amount, MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING, NULL)
2929
ZEND_ARG_TYPE_INFO(0, currency, IS_STRING, 0)
3030
ZEND_END_ARG_INFO()
3131

ext/intl/formatter/formatter_format.c

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ PHP_FUNCTION( numfmt_format )
161161
/* {{{ Format a number as currency. */
162162
PHP_FUNCTION( numfmt_format_currency )
163163
{
164-
double number;
164+
zval *number;
165165
UChar format_buf[32];
166166
UChar* formatted = format_buf;
167167
int32_t formatted_len = USIZE(format_buf);
@@ -171,11 +171,20 @@ PHP_FUNCTION( numfmt_format_currency )
171171
int32_t scurrency_len = 0;
172172
FORMATTER_METHOD_INIT_VARS;
173173

174+
object = getThis();
175+
174176
/* Parse parameters. */
175-
if( zend_parse_method_parameters( ZEND_NUM_ARGS(), getThis(), "Ods",
176-
&object, NumberFormatter_ce_ptr, &number, &currency, &currency_len ) == FAILURE )
177-
{
178-
RETURN_THROWS();
177+
if (object) {
178+
ZEND_PARSE_PARAMETERS_START(2, 2)
179+
Z_PARAM_STR_OR_NUMBER(number)
180+
Z_PARAM_STRING(currency, currency_len)
181+
ZEND_PARSE_PARAMETERS_END();
182+
} else {
183+
ZEND_PARSE_PARAMETERS_START(3, 3)
184+
Z_PARAM_OBJECT_OF_CLASS(object, NumberFormatter_ce_ptr)
185+
Z_PARAM_STR_OR_NUMBER(number)
186+
Z_PARAM_STRING(currency, currency_len)
187+
ZEND_PARSE_PARAMETERS_END();
179188
}
180189

181190
/* Fetch the object. */
@@ -184,9 +193,16 @@ PHP_FUNCTION( numfmt_format_currency )
184193
/* Convert currency to UTF-16. */
185194
intl_convert_utf8_to_utf16(&scurrency, &scurrency_len, currency, currency_len, &INTL_DATA_ERROR_CODE(nfo));
186195
INTL_METHOD_CHECK_STATUS( nfo, "Currency conversion to UTF-16 failed" );
196+
unum_setTextAttribute(FORMATTER_OBJECT(nfo), UNUM_CURRENCY_CODE, scurrency, scurrency_len, &INTL_DATA_ERROR_CODE(nfo));
197+
INTL_METHOD_CHECK_STATUS( nfo, "Setting currency code failed" );
187198

188199
/* Format the number using a fixed-length buffer. */
189-
formatted_len = unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), number, scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
200+
if (Z_TYPE_P(number) == IS_STRING) {
201+
formatted_len = unum_formatDecimal(FORMATTER_OBJECT(nfo), Z_STRVAL_P(number), Z_STRLEN_P(number), formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
202+
} else {
203+
convert_to_double(number);
204+
formatted_len = unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), Z_DVAL_P(number), scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
205+
}
190206

191207
/* If the buffer turned out to be too small
192208
* then allocate another buffer dynamically
@@ -195,7 +211,11 @@ PHP_FUNCTION( numfmt_format_currency )
195211
if (INTL_DATA_ERROR_CODE(nfo) == U_BUFFER_OVERFLOW_ERROR) {
196212
intl_error_reset(INTL_DATA_ERROR_P(nfo));
197213
formatted = eumalloc(formatted_len);
198-
unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), number, scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
214+
if (Z_TYPE_P(number) == IS_STRING) {
215+
unum_formatDecimal(FORMATTER_OBJECT(nfo), Z_STRVAL_P(number), Z_STRLEN_P(number), formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
216+
} else {
217+
unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), Z_DVAL_P(number), scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
218+
}
199219
}
200220

201221
if( U_FAILURE( INTL_DATA_ERROR_CODE((nfo)) ) ) {

ext/intl/php_intl.stub.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ function numfmt_format(NumberFormatter $formatter, int|float|string $num, int $t
230230
/** @param int $offset */
231231
function numfmt_parse(NumberFormatter $formatter, string $string, int $type = NumberFormatter::TYPE_DOUBLE, &$offset = null): int|float|false {}
232232

233-
function numfmt_format_currency(NumberFormatter $formatter, float $amount, string $currency): string|false {}
233+
function numfmt_format_currency(NumberFormatter $formatter, int|float|string $amount, string $currency): string|false {}
234234

235235
/**
236236
* @param string $currency

ext/intl/php_intl_arginfo.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ ZEND_END_ARG_INFO()
388388

389389
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_numfmt_format_currency, 0, 3, MAY_BE_STRING|MAY_BE_FALSE)
390390
ZEND_ARG_OBJ_INFO(0, formatter, NumberFormatter, 0)
391-
ZEND_ARG_TYPE_INFO(0, amount, IS_DOUBLE, 0)
391+
ZEND_ARG_TYPE_MASK(0, amount, MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING, NULL)
392392
ZEND_ARG_TYPE_INFO(0, currency, IS_STRING, 0)
393393
ZEND_END_ARG_INFO()
394394

ext/intl/tests/bug76093.phpt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Bug #76093 (NumberFormatter::format loses precision)
77

88
# See also https://phabricator.wikimedia.org/T268456
99
$x = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
10+
$x2 = new NumberFormatter('en_US', NumberFormatter::CURRENCY);
1011
foreach ([
1112
'999999999999999999', # Fits in signed 64-bit integer
1213
'9999999999999999999', # Does not fit in signed 64-bit integer
@@ -25,6 +26,9 @@ foreach ([
2526
'int64' => $x->format($value, NumberFormatter::TYPE_INT64),
2627
'double' => $x->format($value, NumberFormatter::TYPE_DOUBLE),
2728
'decimal' => $x->format($value, NumberFormatter::TYPE_DECIMAL),
29+
# formatCurrency requires the NumberFormatter to be created with
30+
# the CURRENCY or CURRENCY_ACCOUNTING style.
31+
'currency' => $x2->formatCurrency($value, 'USD'),
2832
]);
2933
} catch (TypeError $ex) {
3034
echo $ex->getMessage(), PHP_EOL;
@@ -33,7 +37,7 @@ foreach ([
3337

3438
?>
3539
--EXPECTF--
36-
array(5) {
40+
array(6) {
3741
["input"]=>
3842
string(18) "999999999999999999"
3943
["default"]=>
@@ -44,8 +48,10 @@ array(5) {
4448
string(25) "1,000,000,000,000,000,000"
4549
["decimal"]=>
4650
string(23) "999,999,999,999,999,999"
51+
["currency"]=>
52+
string(27) "$999,999,999,999,999,999.00"
4753
}
48-
array(5) {
54+
array(6) {
4955
["input"]=>
5056
string(19) "9999999999999999999"
5157
["default"]=>
@@ -56,8 +62,10 @@ array(5) {
5662
string(26) "10,000,000,000,000,000,000"
5763
["decimal"]=>
5864
string(25) "9,999,999,999,999,999,999"
65+
["currency"]=>
66+
string(29) "$9,999,999,999,999,999,999.00"
5967
}
60-
array(5) {
68+
array(6) {
6169
["input"]=>
6270
float(1.0E+19)
6371
["default"]=>
@@ -68,4 +76,6 @@ array(5) {
6876
string(26) "10,000,000,000,000,000,000"
6977
["decimal"]=>
7078
string(26) "10,000,000,000,000,000,000"
79+
["currency"]=>
80+
string(30) "$10,000,000,000,000,000,000.00"
7181
}

ext/intl/tests/bug78912.phpt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ if (version_compare(INTL_ICU_VERSION, '53.0') < 0) die('skip for ICU >= 53.0');
1010
<?php
1111
$nf = new NumberFormatter('en_US', NumberFormatter::CURRENCY_ACCOUNTING);
1212
var_dump($nf->formatCurrency(-12345.67, 'USD'));
13+
# Also some tests for bug #76093 while we're at it.
14+
var_dump($nf->formatCurrency(9999999999999999999, 'USD')); # gets rounded
15+
var_dump($nf->formatCurrency('-12345.67', 'USD'));
16+
var_dump($nf->formatCurrency('9999999999999999999', 'USD')); # not rounded!
1317
?>
1418
--EXPECT--
1519
string(12) "($12,345.67)"
20+
string(30) "$10,000,000,000,000,000,000.00"
21+
string(12) "($12,345.67)"
22+
string(29) "$9,999,999,999,999,999,999.00"

0 commit comments

Comments
 (0)