Skip to content

Commit a14b9f3

Browse files
committed
ext/intl: Add IntlNumberFormatter skeleton based formatter.
Add IntlNumberFormatter::createFromSkeleton() so format() can render units such as "5 km/h", which NumberFormatter cannot do. Needs ICU 63+. Fix GH-24165 Close GH-24266
1 parent 6391979 commit a14b9f3

18 files changed

Lines changed: 644 additions & 2 deletions

‎NEWS‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ PHP NEWS
3232
. Fixed a double-free when Calendar::createInstance() or
3333
IntlCalendar::fromDateTime() fails after adopting a TimeZone.
3434
(Ilia Alshanetsky)
35+
. Added IntlNumberFormatter::createFromSkeleton() (GH-24165). (David Carlier)
3536

3637
- Lexbor:
3738
. Merge patches lexbor/lexbor@8a14bc0 and lexbor/lexbor@f67ce4b, fixing a

‎UPGRADING‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@ PHP 8.7 UPGRADE NOTES
9494
7. New Classes and Interfaces
9595
========================================
9696

97+
- Intl:
98+
. Added IntlNumberFormatter class to format a number with a given locale
99+
and ICU number skeleton, which also covers measurement units, e.g.
100+
"measure-unit/speed-kilometer-per-hour unit-width-full-name".
101+
IntlNumberFormatter::format() accepts an int, a float or a numeric
102+
string, the latter being formatted as a decimal of arbitrary precision.
103+
It is supported from icu 63.
104+
97105
========================================
98106
8. Removed Extensions and SAPIs
99107
========================================

‎ext/intl/CREDITS‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
Internationalization
2-
Ed Batutis, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar
2+
Ed Batutis, David Carlier, Vladimir Iordanov, Dmitry Lakhtyuk, Stanislav Malyshev, Vadim Savchuk, Kirti Velankar

‎ext/intl/config.m4‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ if test "$PHP_INTL" != "no"; then
4848
grapheme/grapheme_string.cpp \
4949
grapheme/grapheme_util.cpp \
5050
msgformat/msgformat_helpers.cpp \
51+
numberformatter/numberformatter_class.cpp \
5152
rangeformatter/rangeformatter_class.cpp \
5253
timezone/timezone_class.cpp \
5354
timezone/timezone_methods.cpp \
@@ -126,6 +127,7 @@ if test "$PHP_INTL" != "no"; then
126127
$ext_builddir/listformatter
127128
$ext_builddir/msgformat
128129
$ext_builddir/normalizer
130+
$ext_builddir/numberformatter
129131
$ext_builddir/rangeformatter
130132
$ext_builddir/resourcebundle
131133
$ext_builddir/spoofchecker

‎ext/intl/config.w32‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ if (PHP_INTL != "no") {
6363
normalizer_class.cpp \
6464
normalizer_normalize.cpp \
6565
", "intl");
66+
ADD_SOURCES(configure_module_dirname + "/numberformatter", "\
67+
numberformatter_class.cpp \
68+
", "intl");
6669
ADD_SOURCES(configure_module_dirname + "/rangeformatter", "\
6770
rangeformatter_class.cpp \
6871
", "intl");
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/** @generate-class-entries */
4+
5+
/**
6+
* @not-serializable
7+
* @strict-properties
8+
*/
9+
final class IntlNumberFormatter {
10+
private function __construct() {}
11+
12+
public static function createFromSkeleton(string $skeleton, string $locale): ?IntlNumberFormatter {}
13+
14+
public function format(float|int|string $value): string|false {}
15+
16+
public function getErrorCode(): int {}
17+
18+
public function getErrorMessage(): string {}
19+
}

‎ext/intl/numberformatter/numberformatter_arginfo.h‎

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
+----------------------------------------------------------------------+
3+
| Copyright © The PHP Group and Contributors. |
4+
+----------------------------------------------------------------------+
5+
| This source file is subject to the Modified BSD License that is |
6+
| bundled with this package in the file LICENSE, and is available |
7+
| through the World Wide Web at <https://www.php.net/license/>. |
8+
| |
9+
| SPDX-License-Identifier: BSD-3-Clause |
10+
+----------------------------------------------------------------------+
11+
| Authors: David Carlier <devnexen@gmail.com> |
12+
+----------------------------------------------------------------------+
13+
*/
14+
15+
extern "C" {
16+
#include "php.h"
17+
#include "zend_API.h"
18+
#include "../intl_common.h"
19+
}
20+
21+
#if U_ICU_VERSION_MAJOR_NUM >= 63
22+
#include <unicode/numberformatter.h>
23+
#include <unicode/unistr.h>
24+
#include <unicode/locid.h>
25+
#include "../intl_convertcpp.h"
26+
27+
extern "C" {
28+
#include "../php_intl.h"
29+
#include "numberformatter_arginfo.h"
30+
}
31+
#include "numberformatter_class.h"
32+
33+
using icu::number::NumberFormatter;
34+
using icu::number::UnlocalizedNumberFormatter;
35+
using icu::UnicodeString;
36+
37+
static zend_object_handlers numberformatter_handlers;
38+
zend_class_entry *IntlNumberFormatter_ce_ptr;
39+
40+
static void numberformatter_free_obj(zend_object *object)
41+
{
42+
IntlNumberFormatter_object *nfo = php_intl_numberformatter_fetch_object(object);
43+
44+
delete NUMBERFORMATTER_OBJECT(nfo);
45+
NUMBERFORMATTER_OBJECT(nfo) = nullptr;
46+
intl_error_reset(INTL_DATA_ERROR_P(nfo));
47+
48+
zend_object_std_dtor(&nfo->zo);
49+
}
50+
51+
static zend_object *numberformatter_create_object(zend_class_entry *ce)
52+
{
53+
IntlNumberFormatter_object *nfo = reinterpret_cast<IntlNumberFormatter_object *>(zend_object_alloc(sizeof(IntlNumberFormatter_object), ce));
54+
55+
intl_error_init(INTL_DATA_ERROR_P(nfo));
56+
NUMBERFORMATTER_OBJECT(nfo) = nullptr;
57+
58+
zend_object_std_init(&nfo->zo, ce);
59+
object_properties_init(&nfo->zo, ce);
60+
nfo->zo.handlers = &numberformatter_handlers;
61+
62+
return &nfo->zo;
63+
}
64+
65+
U_CFUNC PHP_METHOD(IntlNumberFormatter, __construct)
66+
{
67+
ZEND_PARSE_PARAMETERS_NONE();
68+
69+
zend_throw_error(nullptr, "Cannot directly construct %s, use createFromSkeleton method instead", ZSTR_VAL(Z_OBJCE_P(ZEND_THIS)->name));
70+
}
71+
72+
U_CFUNC PHP_METHOD(IntlNumberFormatter, createFromSkeleton)
73+
{
74+
char *skeleton, *locale;
75+
size_t skeleton_len, locale_len;
76+
77+
ZEND_PARSE_PARAMETERS_START(2, 2)
78+
Z_PARAM_STRING(skeleton, skeleton_len)
79+
Z_PARAM_STRING(locale, locale_len)
80+
ZEND_PARSE_PARAMETERS_END();
81+
82+
intl_error_reset(nullptr);
83+
84+
INTL_CHECK_LOCALE_LEN(locale_len);
85+
86+
if (locale_len == 0) {
87+
locale = (char *)intl_locale_get_default();
88+
}
89+
90+
if (icu::Locale(locale).getISO3Language()[0] == '\0') {
91+
zend_argument_value_error(2, "\"%s\" is invalid", locale);
92+
RETURN_THROWS();
93+
}
94+
95+
UErrorCode status = U_ZERO_ERROR;
96+
UnicodeString uskeleton;
97+
98+
intl_stringFromChar(uskeleton, skeleton, skeleton_len, &status);
99+
INTL_CHECK_STATUS_OR_NULL(status, "Error converting skeleton to UTF-16");
100+
101+
UnlocalizedNumberFormatter nf = NumberFormatter::forSkeleton(uskeleton, status);
102+
INTL_CHECK_STATUS_OR_NULL(status, "Number formatter creation failed");
103+
104+
object_init_ex(return_value, IntlNumberFormatter_ce_ptr);
105+
NUMBERFORMATTER_OBJECT(Z_INTL_INTLNUMBERFORMATTER_P(return_value)) = new LocalizedNumberFormatter(nf.locale(icu::Locale::createCanonical(locale)));
106+
}
107+
108+
U_CFUNC PHP_METHOD(IntlNumberFormatter, format)
109+
{
110+
zval *value;
111+
112+
ZEND_PARSE_PARAMETERS_START(1, 1)
113+
Z_PARAM_NUMBER_OR_STR(value)
114+
ZEND_PARSE_PARAMETERS_END();
115+
116+
IntlNumberFormatter_object *nfo = Z_INTL_INTLNUMBERFORMATTER_P(ZEND_THIS);
117+
intl_errors_reset(INTL_DATA_ERROR_P(nfo));
118+
119+
UErrorCode status = U_ZERO_ERROR;
120+
UnicodeString result;
121+
122+
if (Z_TYPE_P(value) == IS_STRING) {
123+
if (UNEXPECTED(zend_str_has_nul_byte(Z_STR_P(value)))) {
124+
zend_argument_value_error(1, "must not contain any null bytes");
125+
RETURN_THROWS();
126+
}
127+
if (UNEXPECTED(Z_STRLEN_P(value) > INT32_MAX)) {
128+
zend_argument_value_error(1, "is too long");
129+
RETURN_THROWS();
130+
}
131+
result = NUMBERFORMATTER_OBJECT(nfo)->formatDecimal(icu::StringPiece(Z_STRVAL_P(value), static_cast<int32_t>(Z_STRLEN_P(value))), status).toString(status);
132+
} else if (Z_TYPE_P(value) == IS_DOUBLE) {
133+
result = NUMBERFORMATTER_OBJECT(nfo)->formatDouble(Z_DVAL_P(value), status).toString(status);
134+
} else {
135+
result = NUMBERFORMATTER_OBJECT(nfo)->formatInt(static_cast<int64_t>(Z_LVAL_P(value)), status).toString(status);
136+
}
137+
138+
if (U_FAILURE(status)) {
139+
intl_errors_set(INTL_DATA_ERROR_P(nfo), status, "Number formatting failed");
140+
RETURN_FALSE;
141+
}
142+
143+
zend_string *ret = intl_charFromString(result, &status);
144+
145+
if (!ret) {
146+
intl_errors_set(INTL_DATA_ERROR_P(nfo), status, "Error converting value to UTF-8");
147+
RETURN_FALSE;
148+
}
149+
150+
RETVAL_STR(ret);
151+
}
152+
153+
U_CFUNC PHP_METHOD(IntlNumberFormatter, getErrorCode)
154+
{
155+
ZEND_PARSE_PARAMETERS_NONE();
156+
157+
RETURN_LONG(INTL_DATA_ERROR_CODE(Z_INTL_INTLNUMBERFORMATTER_P(ZEND_THIS)));
158+
}
159+
160+
U_CFUNC PHP_METHOD(IntlNumberFormatter, getErrorMessage)
161+
{
162+
ZEND_PARSE_PARAMETERS_NONE();
163+
164+
RETURN_STR(intl_error_get_message(INTL_DATA_ERROR_P(Z_INTL_INTLNUMBERFORMATTER_P(ZEND_THIS))));
165+
}
166+
167+
void numberformatter_register_class(void)
168+
{
169+
IntlNumberFormatter_ce_ptr = register_class_IntlNumberFormatter();
170+
IntlNumberFormatter_ce_ptr->create_object = numberformatter_create_object;
171+
172+
memcpy(&numberformatter_handlers, zend_get_std_object_handlers(), sizeof(zend_object_handlers));
173+
numberformatter_handlers.offset = offsetof(IntlNumberFormatter_object, zo);
174+
numberformatter_handlers.free_obj = numberformatter_free_obj;
175+
numberformatter_handlers.clone_obj = nullptr;
176+
}
177+
#endif
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
+----------------------------------------------------------------------+
3+
| Copyright © The PHP Group and Contributors. |
4+
+----------------------------------------------------------------------+
5+
| This source file is subject to the Modified BSD License that is |
6+
| bundled with this package in the file LICENSE, and is available |
7+
| through the World Wide Web at <https://www.php.net/license/>. |
8+
| |
9+
| SPDX-License-Identifier: BSD-3-Clause |
10+
+----------------------------------------------------------------------+
11+
| Authors: David Carlier <devnexen@gmail.com> |
12+
+----------------------------------------------------------------------+
13+
*/
14+
15+
#ifndef NUMBERFORMATTER_CLASS_H
16+
#define NUMBERFORMATTER_CLASS_H
17+
18+
#ifdef __cplusplus
19+
extern "C" {
20+
#endif
21+
#include <php.h>
22+
#include "intl_common.h"
23+
#include "intl_error.h"
24+
#include "intl_data.h"
25+
#ifdef __cplusplus
26+
}
27+
#endif
28+
29+
#ifdef __cplusplus
30+
#include <unicode/numberformatter.h>
31+
using icu::number::LocalizedNumberFormatter;
32+
#else
33+
typedef void LocalizedNumberFormatter;
34+
#endif
35+
36+
typedef struct {
37+
intl_error error;
38+
LocalizedNumberFormatter *unumf;
39+
} numberformatter_data;
40+
41+
typedef struct {
42+
numberformatter_data nf_data;
43+
zend_object zo;
44+
} IntlNumberFormatter_object;
45+
46+
#define php_intl_numberformatter_fetch_object(obj) ZEND_CONTAINER_OF(obj, IntlNumberFormatter_object, zo)
47+
#define Z_INTL_INTLNUMBERFORMATTER_P(zv) php_intl_numberformatter_fetch_object(Z_OBJ_P(zv))
48+
49+
#define NUMBERFORMATTER_OBJECT(nfo) (nfo)->nf_data.unumf
50+
51+
#ifdef __cplusplus
52+
extern "C" {
53+
#endif
54+
void numberformatter_register_class(void);
55+
extern zend_class_entry *IntlNumberFormatter_ce_ptr;
56+
#ifdef __cplusplus
57+
}
58+
#endif
59+
60+
#endif

‎ext/intl/php_intl.c‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#include "locale/locale_class.h"
4343

4444
#include "listformatter/listformatter_class.h"
45+
#include "numberformatter/numberformatter_class.h"
4546
#include "rangeformatter/rangeformatter_class.h"
4647

4748
#include "dateformat/dateformat.h"
@@ -190,6 +191,9 @@ PHP_MINIT_FUNCTION( intl )
190191
listformatter_register_class( );
191192

192193
#if U_ICU_VERSION_MAJOR_NUM >= 63
194+
/* Register 'IntlNumberFormatter' PHP class */
195+
numberformatter_register_class( );
196+
193197
/* Register 'NumberRangeFormatter' PHP class */
194198
rangeformatter_register_class( );
195199
#endif

0 commit comments

Comments
 (0)