Skip to content

Commit 5fbb598

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 12b112d commit 5fbb598

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
@@ -153,7 +153,7 @@ PHP_FUNCTION( numfmt_format )
153153
/* {{{ Format a number as currency. */
154154
PHP_FUNCTION( numfmt_format_currency )
155155
{
156-
double number;
156+
zval *number;
157157
UChar format_buf[32];
158158
UChar* formatted = format_buf;
159159
int32_t formatted_len = USIZE(format_buf);
@@ -163,11 +163,20 @@ PHP_FUNCTION( numfmt_format_currency )
163163
int32_t scurrency_len = 0;
164164
FORMATTER_METHOD_INIT_VARS;
165165

166+
object = getThis();
167+
166168
/* Parse parameters. */
167-
if( zend_parse_method_parameters( ZEND_NUM_ARGS(), getThis(), "Ods",
168-
&object, NumberFormatter_ce_ptr, &number, &currency, &currency_len ) == FAILURE )
169-
{
170-
RETURN_THROWS();
169+
if (object) {
170+
ZEND_PARSE_PARAMETERS_START(2, 2)
171+
Z_PARAM_STR_OR_NUMBER(number)
172+
Z_PARAM_STRING(currency, currency_len)
173+
ZEND_PARSE_PARAMETERS_END();
174+
} else {
175+
ZEND_PARSE_PARAMETERS_START(3, 3)
176+
Z_PARAM_OBJECT_OF_CLASS(object, NumberFormatter_ce_ptr)
177+
Z_PARAM_STR_OR_NUMBER(number)
178+
Z_PARAM_STRING(currency, currency_len)
179+
ZEND_PARSE_PARAMETERS_END();
171180
}
172181

173182
/* Fetch the object. */
@@ -176,9 +185,16 @@ PHP_FUNCTION( numfmt_format_currency )
176185
/* Convert currency to UTF-16. */
177186
intl_convert_utf8_to_utf16(&scurrency, &scurrency_len, currency, currency_len, &INTL_DATA_ERROR_CODE(nfo));
178187
INTL_METHOD_CHECK_STATUS( nfo, "Currency conversion to UTF-16 failed" );
188+
unum_setTextAttribute(FORMATTER_OBJECT(nfo), UNUM_CURRENCY_CODE, scurrency, scurrency_len, &INTL_DATA_ERROR_CODE(nfo));
189+
INTL_METHOD_CHECK_STATUS( nfo, "Setting currency code failed" );
179190

180191
/* Format the number using a fixed-length buffer. */
181-
formatted_len = unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), number, scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
192+
if (Z_TYPE_P(number) == IS_STRING) {
193+
formatted_len = unum_formatDecimal(FORMATTER_OBJECT(nfo), Z_STRVAL_P(number), Z_STRLEN_P(number), formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
194+
} else {
195+
convert_to_double(number);
196+
formatted_len = unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), Z_DVAL_P(number), scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
197+
}
182198

183199
/* If the buffer turned out to be too small
184200
* then allocate another buffer dynamically
@@ -187,7 +203,11 @@ PHP_FUNCTION( numfmt_format_currency )
187203
if (INTL_DATA_ERROR_CODE(nfo) == U_BUFFER_OVERFLOW_ERROR) {
188204
intl_error_reset(INTL_DATA_ERROR_P(nfo));
189205
formatted = eumalloc(formatted_len);
190-
unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), number, scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
206+
if (Z_TYPE_P(number) == IS_STRING) {
207+
unum_formatDecimal(FORMATTER_OBJECT(nfo), Z_STRVAL_P(number), Z_STRLEN_P(number), formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
208+
} else {
209+
unum_formatDoubleCurrency(FORMATTER_OBJECT(nfo), Z_DVAL_P(number), scurrency, formatted, formatted_len, NULL, &INTL_DATA_ERROR_CODE(nfo));
210+
}
191211
}
192212

193213
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
@@ -19,6 +20,9 @@ foreach ([
1920
'int64' => $x->format($value, NumberFormatter::TYPE_INT64),
2021
'double' => $x->format($value, NumberFormatter::TYPE_DOUBLE),
2122
'decimal' => $x->format($value, NumberFormatter::TYPE_DECIMAL),
23+
# formatCurrency requires the NumberFormatter to be created with
24+
# the CURRENCY or CURRENCY_ACCOUNTING style.
25+
'currency' => $x2->formatCurrency($value, 'USD'),
2226
]);
2327
} catch (TypeError $ex) {
2428
echo $ex->getMessage(), PHP_EOL;
@@ -27,7 +31,7 @@ foreach ([
2731

2832
?>
2933
--EXPECT--
30-
array(5) {
34+
array(6) {
3135
["input"]=>
3236
string(18) "999999999999999999"
3337
["default"]=>
@@ -38,8 +42,10 @@ array(5) {
3842
string(25) "1,000,000,000,000,000,000"
3943
["decimal"]=>
4044
string(23) "999,999,999,999,999,999"
45+
["currency"]=>
46+
string(27) "$999,999,999,999,999,999.00"
4147
}
42-
array(5) {
48+
array(6) {
4349
["input"]=>
4450
string(19) "9999999999999999999"
4551
["default"]=>
@@ -50,8 +56,10 @@ array(5) {
5056
string(26) "10,000,000,000,000,000,000"
5157
["decimal"]=>
5258
string(25) "9,999,999,999,999,999,999"
59+
["currency"]=>
60+
string(29) "$9,999,999,999,999,999,999.00"
5361
}
54-
array(5) {
62+
array(6) {
5563
["input"]=>
5664
float(1.0E+19)
5765
["default"]=>
@@ -62,4 +70,6 @@ array(5) {
6270
string(26) "10,000,000,000,000,000,000"
6371
["decimal"]=>
6472
string(26) "10,000,000,000,000,000,000"
73+
["currency"]=>
74+
string(30) "$10,000,000,000,000,000,000.00"
6575
}

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)