Skip to content

Fix #76093: Format strings w/o loss of precision w/ FORMAT_TYPE_DECIMAL #9909

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions Zend/zend_API.h
Original file line number Diff line number Diff line change
Expand Up @@ -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") \
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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
) {
Expand Down
9 changes: 7 additions & 2 deletions ext/intl/formatter/formatter.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -421,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
Expand Down
12 changes: 9 additions & 3 deletions ext/intl/formatter/formatter_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

95 changes: 82 additions & 13 deletions ext/intl/formatter/formatter_format.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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);
Expand All @@ -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));
Expand All @@ -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;
Expand All @@ -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();
Expand All @@ -125,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);
Expand All @@ -135,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, &currency, &currency_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. */
Expand All @@ -148,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
Expand All @@ -159,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)) ) ) {
Expand Down
1 change: 1 addition & 0 deletions ext/intl/formatter/formatter_format.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions ext/intl/php_intl.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -390,12 +390,12 @@ 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 {}

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
Expand Down
6 changes: 3 additions & 3 deletions ext/intl/php_intl_arginfo.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 34 additions & 4 deletions ext/intl/tests/bug48227.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,50 @@ intl
--FILE--
<?php

$testcases = ['', 1, NULL, true, false, [], (object)[]];

echo("OOP\n");
$x = new NumberFormatter('en_US', NumberFormatter::DECIMAL);
foreach (['', 1, NULL, $x] as $value) {
foreach (array_merge($testcases, [$x]) as $value) {
try {
var_dump($x->format($value));
} catch (TypeError $ex) {
echo $ex->getMessage(), PHP_EOL;
}
}

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
Loading