Skip to content

Commit 5a6f6e5

Browse files
authored
feat: add currency property and option (#12)
1 parent 23fd04e commit 5a6f6e5

38 files changed

+522
-68
lines changed

docs/readthedocs/reference/ecma-intl-locale-options.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ Ecma\\Intl\\Locale\\Options
3838
3939
The collation algorithm to set on the locale.
4040

41+
.. php:attr:: currency: string | null, readonly
42+
43+
The currency to set on the locale.
44+
4145
.. php:attr:: hourCycle: string | null, readonly
4246
4347
The hour cycle to set on the locale.
@@ -68,6 +72,7 @@ Ecma\\Intl\\Locale\\Options
6872
:param Stringable | string | null $calendar: The calendar to use with the locale.
6973
:param Stringable | string | false | null $caseFirst: The case sorting algorithm to use with the locale.
7074
:param Stringable | string | null $collation: The collation algorithm to use with the locale.
75+
:param Stringable | string | null $currency: The currency to set on the locale.
7176
:param Stringable | string | null $hourCycle: The hour cycle to use with the locale.
7277
:param Stringable | string | null $language: The language to use with the locale.
7378
:param Stringable | string | null $numberingSystem: The numbering system to use with the locale.

docs/readthedocs/reference/ecma-intl-locale.rst

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ Ecma\\Intl\\Locale
6060
6161
See :php:meth:`Ecma\\Intl\\Locale::getCollations()`.
6262

63+
.. php:attr:: currencies: string[], readonly
64+
65+
See :php:meth:`Ecma\\Intl\\Locale::getCurrencies()`.
66+
67+
.. php:attr:: currency: string | null, readonly
68+
69+
The ``currency`` property has the currency type for this locale.
70+
71+
The currency type is the 3-character ISO 4217 currency code.
72+
If neither the ``cu`` key of the locale identifier nor the
73+
:php:attr:`Ecma\\Intl\\Locale\\Options::$currency` option is set, this
74+
value is `null`.
75+
76+
This property is not defined in ECMA-402 or in the `Intl Locale Info
77+
Proposal <https://tc39.es/proposal-intl-locale-info/>`_. Instead, this
78+
is unique to the PHP implementation and is inspired by the
79+
Intl Locale Info Proposal.
80+
6381
.. php:attr:: hourCycle: string | null, readonly
6482
6583
The ``hourCycle`` property has the hour cycle type for this locale.
@@ -197,11 +215,14 @@ Ecma\\Intl\\Locale
197215
Returns a list of one or more currency types commonly used for this
198216
locale.
199217

218+
If the locale already includes a currency (e.g., ``en-u-cu-eur``) or
219+
one was provided via the constructor's ``$options`` parameter, this
220+
list will contain only that currency type.
221+
200222
This method is not defined in ECMA-402 or in the `Intl Locale Info
201223
Proposal <https://tc39.es/proposal-intl-locale-info/>`_ in which other
202224
similar methods are described. Instead, this is unique to the PHP
203-
implementation and draws its inspiration from the Intl Locale Info
204-
Proposal.
225+
implementation and is inspired by the Intl Locale Info Proposal.
205226

206227
.. php:method:: getHourCycles(): string[]
207228

src/common.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#define BCP47_KEYWORD_CALENDAR "ca"
1717
#define BCP47_KEYWORD_CASE_FIRST "kf"
1818
#define BCP47_KEYWORD_COLLATION "co"
19+
#define BCP47_KEYWORD_CURRENCY "cu"
1920
#define BCP47_KEYWORD_HOUR_CYCLE "hc"
2021
#define BCP47_KEYWORD_NUMBERING_SYSTEM "nu"
2122
#define BCP47_KEYWORD_NUMERIC "kn"

src/ecma402/language_tag.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ bool ecma402_isStructurallyValidLanguageTag(const char *tag) {
4343
return parser.parseUnicodeLocaleId();
4444
}
4545

46+
bool ecma402_isUnicodeCurrencyType(const char *currency) {
47+
return ecma402::isUnicodeCurrencyType(currency);
48+
}
49+
4650
bool ecma402_isUnicodeLanguageSubtag(const char *language) {
4751
return ecma402::isUnicodeLanguageSubtag(language);
4852
}
@@ -77,6 +81,11 @@ bool ecma402_isUnicodeScriptSubtag(const char *script) {
7781
return ecma402::isUnicodeScriptSubtag(script);
7882
}
7983

84+
bool ecma402::isUnicodeCurrencyType(const std::string &string) {
85+
return string.length() == 3 &&
86+
std::all_of(string.cbegin(), string.cend(), util::isAsciiAlpha);
87+
}
88+
8089
bool ecma402::isUnicodeLanguageSubtag(const std::string &string) {
8190
auto length = string.length();
8291
return length >= 2 && length <= 8 && length != 4 &&

src/ecma402/language_tag.h

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ extern "C" {
2929
*/
3030
bool ecma402_isStructurallyValidLanguageTag(const char *tag);
3131

32+
/**
33+
* Returns true if the string is a valid Unicode currency type.
34+
*/
35+
bool ecma402_isUnicodeCurrencyType(const char *currency);
36+
3237
/**
3338
* Returns true if the string is a valid Unicode language subtag.
3439
*/
@@ -63,6 +68,18 @@ bool ecma402_isUnicodeScriptSubtag(const char *script);
6368

6469
namespace ecma402 {
6570

71+
/**
72+
* Returns true if the string is a valid Unicode language subtag.
73+
*
74+
* <p>The currency type consists of:</p>
75+
*
76+
* <blockquote>
77+
* Codes consisting of 3 ASCII letters that are or have been valid in ISO 4217,
78+
* plus certain additional codes that are or have been in common use.
79+
* </blockquote>
80+
*/
81+
bool isUnicodeCurrencyType(const std::string &string);
82+
6683
/**
6784
* Returns true if the string is a valid Unicode language subtag.
6885
*

src/ecma402/locale.cpp

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,12 +67,11 @@ int getTimeZonesForLocale(char *localeId, const char **values);
6767

6868
} // namespace
6969

70-
ecma402_locale *
71-
ecma402_applyLocaleOptions(ecma402_locale *locale, const char *calendar,
72-
const char *caseFirst, const char *collation,
73-
const char *hourCycle, const char *language,
74-
const char *numberingSystem, int numeric,
75-
const char *region, const char *script) {
70+
ecma402_locale *ecma402_applyLocaleOptions(
71+
ecma402_locale *locale, const char *calendar, const char *caseFirst,
72+
const char *collation, const char *currency, const char *hourCycle,
73+
const char *language, const char *numberingSystem, int numeric,
74+
const char *region, const char *script) {
7675
icu::Locale icuLocale;
7776
icu::LocaleBuilder icuLocaleBuilder;
7877
UErrorCode icuStatus = U_ZERO_ERROR;
@@ -98,6 +97,10 @@ ecma402_applyLocaleOptions(ecma402_locale *locale, const char *calendar,
9897
collation);
9998
}
10099

100+
if (currency != nullptr) {
101+
icuLocaleBuilder.setUnicodeLocaleKeyword(BCP47_KEYWORD_CURRENCY, currency);
102+
}
103+
101104
if (hourCycle != nullptr) {
102105
icuLocaleBuilder.setUnicodeLocaleKeyword(BCP47_KEYWORD_HOUR_CYCLE,
103106
hourCycle);
@@ -261,6 +264,7 @@ void ecma402_freeLocale(ecma402_locale *locale) {
261264
FREE_PROPERTY(canonical);
262265
FREE_PROPERTY(caseFirst);
263266
FREE_PROPERTY(collation);
267+
FREE_PROPERTY(currency);
264268
FREE_PROPERTY(hourCycle);
265269
FREE_PROPERTY(language);
266270
FREE_PROPERTY(numberingSystem);
@@ -326,6 +330,70 @@ int ecma402_getCollation(const char *localeId, char *collation,
326330
isCanonicalized);
327331
}
328332

333+
int ecma402_getCurrency(const char *localeId, char *currency,
334+
ecma402_errorStatus *status, bool isCanonicalized) {
335+
char *canonicalized;
336+
UChar buffer[4];
337+
UErrorCode icuStatus = U_ZERO_ERROR;
338+
std::string icuValue;
339+
int icuValueLength;
340+
341+
if (localeId == nullptr) {
342+
return -1;
343+
}
344+
345+
if (isCanonicalized) {
346+
canonicalized = strdup(localeId);
347+
} else {
348+
canonicalized = (char *)malloc(sizeof(char) * ULOC_FULLNAME_CAPACITY);
349+
ecma402_canonicalizeUnicodeLocaleId(localeId, canonicalized, status);
350+
351+
if (ecma402_hasError(status)) {
352+
free(canonicalized);
353+
return -1;
354+
}
355+
}
356+
357+
// If given a locale like "en-US-u-cu-foobar," ucurr_forLocale() will return
358+
// the default currency for the locale (e.g., "USD"), and if given a locale
359+
// like "en-US-u-cu-fo," it will return "YES," but we would prefer it to
360+
// indicate it couldn't find a default currency, since the user provided one,
361+
// so we do this check here to see if the "cu" user-provided value is exactly
362+
// 3 alphanumeric characters. If it is not, we return -1.
363+
std::string const canonicalStr(canonicalized);
364+
free(canonicalized);
365+
366+
size_t const cuPos = canonicalStr.find("-cu-");
367+
if (cuPos != std::string::npos) {
368+
size_t const startPos = cuPos + 4;
369+
size_t const endPos = canonicalStr.find('-', startPos);
370+
size_t const cuLen =
371+
(endPos == std::string::npos) ? std::string::npos : endPos - startPos;
372+
373+
std::string const cuStr = canonicalStr.substr(startPos, cuLen);
374+
if (cuStr.length() != 3) {
375+
return -1;
376+
}
377+
} else {
378+
// The locale does not specify a currency.
379+
return -1;
380+
}
381+
382+
icuValueLength = ucurr_forLocale(canonicalStr.c_str(), buffer, 4, &icuStatus);
383+
384+
if (U_FAILURE(icuStatus) != U_ZERO_ERROR) {
385+
return -1;
386+
}
387+
388+
for (int i = 0; i < icuValueLength; i++) {
389+
icuValue.push_back(buffer[i]);
390+
}
391+
392+
memcpy(currency, icuValue.c_str(), icuValue.length() + 1);
393+
394+
return icuValue.length();
395+
}
396+
329397
int ecma402_getHourCycle(const char *localeId, char *hourCycle,
330398
ecma402_errorStatus *status, bool isCanonicalized) {
331399
return getKeywordValue(ICU_KEYWORD_HOUR_CYCLE, localeId, hourCycle, status,
@@ -371,6 +439,7 @@ ecma402_locale *ecma402_initEmptyLocale(void) {
371439
locale->canonical = nullptr;
372440
locale->caseFirst = nullptr;
373441
locale->collation = nullptr;
442+
locale->currency = nullptr;
374443
locale->hourCycle = nullptr;
375444
locale->language = nullptr;
376445
locale->numberingSystem = nullptr;
@@ -412,6 +481,7 @@ ecma402_locale *ecma402_initLocale(const char *localeId) {
412481
INIT_PROPERTY(canonical, calendar, ULOC_KEYWORDS_CAPACITY, getCalendar);
413482
INIT_PROPERTY(canonical, caseFirst, ULOC_KEYWORDS_CAPACITY, getCaseFirst);
414483
INIT_PROPERTY(canonical, collation, ULOC_KEYWORDS_CAPACITY, getCollation);
484+
INIT_PROPERTY(canonical, currency, 4, getCurrency);
415485
INIT_PROPERTY(canonical, hourCycle, ULOC_KEYWORDS_CAPACITY, getHourCycle);
416486
INIT_PROPERTY(canonical, language, ULOC_LANG_CAPACITY, getLanguage);
417487
INIT_PROPERTY(canonical, numberingSystem, ULOC_KEYWORDS_CAPACITY,
@@ -468,8 +538,7 @@ int ecma402_keywordsOfLocale(ecma402_locale *locale, const char *keyword,
468538

469539
canonical = locale->canonical;
470540

471-
if (strcmp(keyword, ICU_KEYWORD_TIME_ZONE) == 0 ||
472-
strcmp(keyword, ICU_KEYWORD_CURRENCY) == 0) {
541+
if (strcmp(keyword, ICU_KEYWORD_TIME_ZONE) == 0) {
473542
// Skip checking for a "preferred" identifier for these keywords.
474543
} else {
475544
// Check to see whether the localeId already has the keyword value set on
@@ -483,7 +552,14 @@ int ecma402_keywordsOfLocale(ecma402_locale *locale, const char *keyword,
483552
return 0;
484553
}
485554

486-
if (preferredLength > 0) {
555+
// If the keyword is "currency," there's some special handling: it must have
556+
// a length of exactly 3, and it must not have a value of "YES" (which
557+
// implies the length was actually less than 3 and ICU converted that to a
558+
// truthy string "YES").
559+
// If the keyword is not "currency," then the length must be greater than 0.
560+
if ((strcmp(keyword, ICU_KEYWORD_CURRENCY) == 0 && preferredLength == 3 &&
561+
strcasecmp(preferred, "YES") != 0) ||
562+
(strcmp(keyword, ICU_KEYWORD_CURRENCY) != 0 && preferredLength > 0)) {
487563
values[0] = strdup(uloc_toUnicodeLocaleType(keyword, preferred));
488564
free(preferred);
489565
return 1;

src/ecma402/locale.h

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ typedef struct ecma402_locale {
5353
*/
5454
char *collation;
5555

56+
/**
57+
* The currency (cu) property of the locale identifier, if available.
58+
*/
59+
char *currency;
60+
5661
/**
5762
* The hours (hc) property of the locale identifier, if available.
5863
*/
@@ -107,6 +112,7 @@ typedef struct ecma402_locale {
107112
* @param calendar The calendar (ca) type to set; use NULL to ignore.
108113
* @param caseFirst The colcasefirst (kf) type to set; use NULL to ignore.
109114
* @param collation The collation (co) type to set; use NULL to ignore.
115+
* @param currency The currency (cu) type to set; use NULL to ignore.
110116
* @param hourCycle The hours (hc) type to set; use NULL to ignore.
111117
* @param language The language type to set; use NULL to ignore.
112118
* @param numberingSystem The numbers (nu) type to set; use NULL to ignore.
@@ -117,9 +123,9 @@ typedef struct ecma402_locale {
117123
ecma402_locale *
118124
ecma402_applyLocaleOptions(ecma402_locale *locale, const char *calendar,
119125
const char *caseFirst, const char *collation,
120-
const char *hourCycle, const char *language,
121-
const char *numberingSystem, int numeric,
122-
const char *region, const char *script);
126+
const char *currency, const char *hourCycle,
127+
const char *language, const char *numberingSystem,
128+
int numeric, const char *region, const char *script);
123129

124130
/**
125131
* Canonicalizes a list of locales.
@@ -256,6 +262,9 @@ int ecma402_getCaseFirst(const char *localeId, char *caseFirst,
256262
int ecma402_getCollation(const char *localeId, char *collation,
257263
ecma402_errorStatus *status, bool isCanonicalized);
258264

265+
int ecma402_getCurrency(const char *localeId, char *currency,
266+
ecma402_errorStatus *status, bool isCanonicalized);
267+
259268
/**
260269
* Returns the value of the hours (hc) keyword for the given locale ID.
261270
*

src/php/classes/locale.c

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ PHP_METHOD(Ecma_Intl_Locale, __construct) {
164164
SET_PROPERTY_STRING(collation);
165165
SET_PROPERTY_ARRAY(collations, ECMA402_LOCALE_COLLATION_CAPACITY);
166166
SET_PROPERTY_ARRAY(currencies, ECMA402_LOCALE_CURRENCY_CAPACITY);
167+
SET_PROPERTY_STRING(currency);
167168
SET_PROPERTY_STRING(hourCycle);
168169
SET_PROPERTY_ARRAY(hourCycles, ECMA402_LOCALE_HOUR_CYCLE_CAPACITY);
169170
SET_PROPERTY_STRING(language);
@@ -223,6 +224,7 @@ PHP_METHOD(Ecma_Intl_Locale, jsonSerialize) {
223224
ADD_TO_JSON(collation);
224225
ADD_TO_JSON(collations);
225226
ADD_TO_JSON(currencies);
227+
ADD_TO_JSON(currency);
226228
ADD_TO_JSON(hourCycle);
227229
ADD_TO_JSON(hourCycles);
228230
ADD_TO_JSON(language);
@@ -276,6 +278,7 @@ static ecma402_locale *applyOptions(ecma402_locale *locale,
276278
const char *calendar = getOption(options, "calendar");
277279
const char *caseFirst = getOption(options, "caseFirst");
278280
const char *collation = getOption(options, "collation");
281+
const char *currency = getOption(options, "currency");
279282
const char *hourCycle = getOption(options, "hourCycle");
280283
const char *language = getOption(options, "language");
281284
const char *numberingSystem = getOption(options, "numberingSystem");
@@ -284,8 +287,8 @@ static ecma402_locale *applyOptions(ecma402_locale *locale,
284287
const char *script = getOption(options, "script");
285288

286289
return ecma402_applyLocaleOptions(locale, calendar, caseFirst, collation,
287-
hourCycle, language, numberingSystem,
288-
numeric, region, script);
290+
currency, hourCycle, language,
291+
numberingSystem, numeric, region, script);
289292
}
290293

291294
static void freeLocaleObj(zend_object *object) {

src/php/classes/locale.stub.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,21 @@
9292
*/
9393
public readonly array $currencies;
9494

95+
/**
96+
* The `currency` property has the currency type for this locale.
97+
*
98+
* The currency type is the 3-character ISO 4217 currency code. If
99+
* neither the `cu` key of the locale identifier nor the `currency`
100+
* property of the {@see Locale\Options} is set, this value is `null`.
101+
*
102+
* This property is not defined in ECMA-402 or in the Intl Locale Info
103+
* Proposal. Instead, this is unique to the PHP implementation and is
104+
* inspired by the Intl Locale Info Proposal.
105+
*
106+
* @link https://tc39.es/proposal-intl-locale-info/ Intl Locale Info Proposal
107+
*/
108+
public readonly ?string $currency;
109+
95110
/**
96111
* The `hourCycle` property has the hour cycle type for this locale.
97112
*
@@ -264,10 +279,14 @@ public function getCollations(): array
264279
* Returns a list of one or more currency types commonly used for this
265280
* locale.
266281
*
282+
* If the locale already includes a currency (e.g., `en-u-cu-eur`) or
283+
* one was provided via the constructor's `$options` parameter, this
284+
* list will contain only that currency type.
285+
*
267286
* This method is not defined in ECMA-402 or in the Intl Locale Info
268287
* Proposal in which other similar methods are described. Instead, this
269-
* is unique to the PHP implementation and draws its inspiration from
270-
* the Intl Locale Info Proposal.
288+
* is unique to the PHP implementation and is inspired by the Intl
289+
* Locale Info Proposal.
271290
*
272291
* @link https://tc39.es/proposal-intl-locale-info/ Intl Locale Info Proposal
273292
* @link https://github.com/tc39/proposal-intl-locale-info/issues/75 Possible of addition of Intl.Locale.prototype.getCurrencies()?

0 commit comments

Comments
 (0)