Skip to content

Commit 93f26bc

Browse files
cscottlucaswerkmeister
authored andcommitted
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 b2cb3da commit 93f26bc

File tree

7 files changed

+53
-16
lines changed

7 files changed

+53
-16
lines changed

ext/intl/formatter/formatter.stub.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ public function parse(string $string, int $type = NumberFormatter::TYPE_DOUBLE,
427427
* @tentative-return-type
428428
* @alias numfmt_format_currency
429429
*/
430-
public function formatCurrency(float $amount, string $currency): string|false {}
430+
public function formatCurrency(int|float|string $amount, string $currency): string|false {}
431431

432432
/**
433433
* @param string $currency

ext/intl/formatter/formatter_arginfo.h

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/intl/formatter/formatter_format.c

+27-7
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ PHP_FUNCTION( numfmt_format )
172172
/* {{{ Format a number as currency. */
173173
PHP_FUNCTION( numfmt_format_currency )
174174
{
175-
double number;
175+
zval *number;
176176
UChar format_buf[32];
177177
UChar* formatted = format_buf;
178178
int32_t formatted_len = USIZE(format_buf);
@@ -182,11 +182,20 @@ PHP_FUNCTION( numfmt_format_currency )
182182
int32_t scurrency_len = 0;
183183
FORMATTER_METHOD_INIT_VARS;
184184

185+
object = getThis();
186+
185187
/* Parse parameters. */
186-
if( zend_parse_method_parameters( ZEND_NUM_ARGS(), getThis(), "Ods",
187-
&object, NumberFormatter_ce_ptr, &number, &currency, &currency_len ) == FAILURE )
188-
{
189-
RETURN_THROWS();
188+
if (object) {
189+
ZEND_PARSE_PARAMETERS_START(2, 2)
190+
Z_PARAM_STR_OR_NUMBER(number)
191+
Z_PARAM_STRING(currency, currency_len)
192+
ZEND_PARSE_PARAMETERS_END();
193+
} else {
194+
ZEND_PARSE_PARAMETERS_START(3, 3)
195+
Z_PARAM_OBJECT_OF_CLASS(object, NumberFormatter_ce_ptr)
196+
Z_PARAM_STR_OR_NUMBER(number)
197+
Z_PARAM_STRING(currency, currency_len)
198+
ZEND_PARSE_PARAMETERS_END();
190199
}
191200

192201
/* Fetch the object. */
@@ -195,9 +204,16 @@ PHP_FUNCTION( numfmt_format_currency )
195204
/* Convert currency to UTF-16. */
196205
intl_convert_utf8_to_utf16(&scurrency, &scurrency_len, currency, currency_len, &INTL_DATA_ERROR_CODE(nfo));
197206
INTL_METHOD_CHECK_STATUS( nfo, "Currency conversion to UTF-16 failed" );
207+
unum_setTextAttribute(FORMATTER_OBJECT(nfo), UNUM_CURRENCY_CODE, scurrency, scurrency_len, &INTL_DATA_ERROR_CODE(nfo));
208+
INTL_METHOD_CHECK_STATUS( nfo, "Setting currency code failed" );
198209

199210
/* Format the number using a fixed-length buffer. */
200-
formatted_len = unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), number, scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
211+
if (Z_TYPE_P(number) == IS_STRING) {
212+
formatted_len = unum_formatDecimal(FORMATTER_OBJECT(nfo), Z_STRVAL_P(number), Z_STRLEN_P(number), formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
213+
} else {
214+
convert_to_double(number);
215+
formatted_len = unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), Z_DVAL_P(number), scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
216+
}
201217

202218
/* If the buffer turned out to be too small
203219
* then allocate another buffer dynamically
@@ -206,7 +222,11 @@ PHP_FUNCTION( numfmt_format_currency )
206222
if (INTL_DATA_ERROR_CODE(nfo) == U_BUFFER_OVERFLOW_ERROR) {
207223
intl_error_reset(INTL_DATA_ERROR_P(nfo));
208224
formatted = eumalloc(formatted_len);
209-
unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), number, scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
225+
if (Z_TYPE_P(number) == IS_STRING) {
226+
unum_formatDecimal(FORMATTER_OBJECT(nfo), Z_STRVAL_P(number), Z_STRLEN_P(number), formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
227+
} else {
228+
unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), Z_DVAL_P(number), scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
229+
}
210230
}
211231

212232
if( U_FAILURE( INTL_DATA_ERROR_CODE((nfo)) ) ) {

ext/intl/php_intl.stub.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ function numfmt_format(NumberFormatter $formatter, int|float|string $num, int $t
395395
/** @param int $offset */
396396
function numfmt_parse(NumberFormatter $formatter, string $string, int $type = NumberFormatter::TYPE_DOUBLE, &$offset = null): int|float|false {}
397397

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

400400
/**
401401
* @param string $currency

ext/intl/php_intl_arginfo.h

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/intl/tests/bug76093.phpt

+13-3
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

+7
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)