Skip to content
Merged
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
5 changes: 5 additions & 0 deletions docs/readthedocs/reference/ecma-intl-locale-options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ Ecma\\Intl\\Locale\\Options

The collation algorithm to set on the locale.

.. php:attr:: currency: string | null, readonly

The currency to set on the locale.

.. php:attr:: hourCycle: string | null, readonly

The hour cycle to set on the locale.
Expand Down Expand Up @@ -68,6 +72,7 @@ Ecma\\Intl\\Locale\\Options
:param Stringable | string | null $calendar: The calendar to use with the locale.
:param Stringable | string | false | null $caseFirst: The case sorting algorithm to use with the locale.
:param Stringable | string | null $collation: The collation algorithm to use with the locale.
:param Stringable | string | null $currency: The currency to set on the locale.
:param Stringable | string | null $hourCycle: The hour cycle to use with the locale.
:param Stringable | string | null $language: The language to use with the locale.
:param Stringable | string | null $numberingSystem: The numbering system to use with the locale.
Expand Down
25 changes: 23 additions & 2 deletions docs/readthedocs/reference/ecma-intl-locale.rst
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,24 @@ Ecma\\Intl\\Locale

See :php:meth:`Ecma\\Intl\\Locale::getCollations()`.

.. php:attr:: currencies: string[], readonly

See :php:meth:`Ecma\\Intl\\Locale::getCurrencies()`.

.. php:attr:: currency: string | null, readonly

The ``currency`` property has the currency type for this locale.

The currency type is the 3-character ISO 4217 currency code.
If neither the ``cu`` key of the locale identifier nor the
:php:attr:`Ecma\\Intl\\Locale\\Options::$currency` option is set, this
value is `null`.

This property is not defined in ECMA-402 or in the `Intl Locale Info
Proposal <https://tc39.es/proposal-intl-locale-info/>`_. Instead, this
is unique to the PHP implementation and is inspired by the
Intl Locale Info Proposal.

.. php:attr:: hourCycle: string | null, readonly

The ``hourCycle`` property has the hour cycle type for this locale.
Expand Down Expand Up @@ -197,11 +215,14 @@ Ecma\\Intl\\Locale
Returns a list of one or more currency types commonly used for this
locale.

If the locale already includes a currency (e.g., ``en-u-cu-eur``) or
one was provided via the constructor's ``$options`` parameter, this
list will contain only that currency type.

This method is not defined in ECMA-402 or in the `Intl Locale Info
Proposal <https://tc39.es/proposal-intl-locale-info/>`_ in which other
similar methods are described. Instead, this is unique to the PHP
implementation and draws its inspiration from the Intl Locale Info
Proposal.
implementation and is inspired by the Intl Locale Info Proposal.

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

Expand Down
1 change: 1 addition & 0 deletions src/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#define BCP47_KEYWORD_CALENDAR "ca"
#define BCP47_KEYWORD_CASE_FIRST "kf"
#define BCP47_KEYWORD_COLLATION "co"
#define BCP47_KEYWORD_CURRENCY "cu"
#define BCP47_KEYWORD_HOUR_CYCLE "hc"
#define BCP47_KEYWORD_NUMBERING_SYSTEM "nu"
#define BCP47_KEYWORD_NUMERIC "kn"
Expand Down
9 changes: 9 additions & 0 deletions src/ecma402/language_tag.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ bool ecma402_isStructurallyValidLanguageTag(const char *tag) {
return parser.parseUnicodeLocaleId();
}

bool ecma402_isUnicodeCurrencyType(const char *currency) {
return ecma402::isUnicodeCurrencyType(currency);
}

bool ecma402_isUnicodeLanguageSubtag(const char *language) {
return ecma402::isUnicodeLanguageSubtag(language);
}
Expand Down Expand Up @@ -77,6 +81,11 @@ bool ecma402_isUnicodeScriptSubtag(const char *script) {
return ecma402::isUnicodeScriptSubtag(script);
}

bool ecma402::isUnicodeCurrencyType(const std::string &string) {
return string.length() == 3 &&
std::all_of(string.cbegin(), string.cend(), util::isAsciiAlpha);
}

bool ecma402::isUnicodeLanguageSubtag(const std::string &string) {
auto length = string.length();
return length >= 2 && length <= 8 && length != 4 &&
Expand Down
17 changes: 17 additions & 0 deletions src/ecma402/language_tag.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ extern "C" {
*/
bool ecma402_isStructurallyValidLanguageTag(const char *tag);

/**
* Returns true if the string is a valid Unicode currency type.
*/
bool ecma402_isUnicodeCurrencyType(const char *currency);

/**
* Returns true if the string is a valid Unicode language subtag.
*/
Expand Down Expand Up @@ -63,6 +68,18 @@ bool ecma402_isUnicodeScriptSubtag(const char *script);

namespace ecma402 {

/**
* Returns true if the string is a valid Unicode language subtag.
*
* <p>The currency type consists of:</p>
*
* <blockquote>
* Codes consisting of 3 ASCII letters that are or have been valid in ISO 4217,
* plus certain additional codes that are or have been in common use.
* </blockquote>
*/
bool isUnicodeCurrencyType(const std::string &string);

/**
* Returns true if the string is a valid Unicode language subtag.
*
Expand Down
94 changes: 85 additions & 9 deletions src/ecma402/locale.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,11 @@ int getTimeZonesForLocale(char *localeId, const char **values);

} // namespace

ecma402_locale *
ecma402_applyLocaleOptions(ecma402_locale *locale, const char *calendar,
const char *caseFirst, const char *collation,
const char *hourCycle, const char *language,
const char *numberingSystem, int numeric,
const char *region, const char *script) {
ecma402_locale *ecma402_applyLocaleOptions(
ecma402_locale *locale, const char *calendar, const char *caseFirst,
const char *collation, const char *currency, const char *hourCycle,
const char *language, const char *numberingSystem, int numeric,
const char *region, const char *script) {
icu::Locale icuLocale;
icu::LocaleBuilder icuLocaleBuilder;
UErrorCode icuStatus = U_ZERO_ERROR;
Expand All @@ -98,6 +97,10 @@ ecma402_applyLocaleOptions(ecma402_locale *locale, const char *calendar,
collation);
}

if (currency != nullptr) {
icuLocaleBuilder.setUnicodeLocaleKeyword(BCP47_KEYWORD_CURRENCY, currency);
}

if (hourCycle != nullptr) {
icuLocaleBuilder.setUnicodeLocaleKeyword(BCP47_KEYWORD_HOUR_CYCLE,
hourCycle);
Expand Down Expand Up @@ -261,6 +264,7 @@ void ecma402_freeLocale(ecma402_locale *locale) {
FREE_PROPERTY(canonical);
FREE_PROPERTY(caseFirst);
FREE_PROPERTY(collation);
FREE_PROPERTY(currency);
FREE_PROPERTY(hourCycle);
FREE_PROPERTY(language);
FREE_PROPERTY(numberingSystem);
Expand Down Expand Up @@ -326,6 +330,70 @@ int ecma402_getCollation(const char *localeId, char *collation,
isCanonicalized);
}

int ecma402_getCurrency(const char *localeId, char *currency,
ecma402_errorStatus *status, bool isCanonicalized) {
char *canonicalized;
UChar buffer[4];
UErrorCode icuStatus = U_ZERO_ERROR;
std::string icuValue;
int icuValueLength;

if (localeId == nullptr) {
return -1;
}

if (isCanonicalized) {
canonicalized = strdup(localeId);
} else {
canonicalized = (char *)malloc(sizeof(char) * ULOC_FULLNAME_CAPACITY);
ecma402_canonicalizeUnicodeLocaleId(localeId, canonicalized, status);

if (ecma402_hasError(status)) {
free(canonicalized);
return -1;
}
}

// If given a locale like "en-US-u-cu-foobar," ucurr_forLocale() will return
// the default currency for the locale (e.g., "USD"), and if given a locale
// like "en-US-u-cu-fo," it will return "YES," but we would prefer it to
// indicate it couldn't find a default currency, since the user provided one,
// so we do this check here to see if the "cu" user-provided value is exactly
// 3 alphanumeric characters. If it is not, we return -1.
std::string const canonicalStr(canonicalized);
free(canonicalized);

size_t const cuPos = canonicalStr.find("-cu-");
if (cuPos != std::string::npos) {
size_t const startPos = cuPos + 4;
size_t const endPos = canonicalStr.find('-', startPos);
size_t const cuLen =
(endPos == std::string::npos) ? std::string::npos : endPos - startPos;

std::string const cuStr = canonicalStr.substr(startPos, cuLen);
if (cuStr.length() != 3) {
return -1;
}
} else {
// The locale does not specify a currency.
return -1;
}

icuValueLength = ucurr_forLocale(canonicalStr.c_str(), buffer, 4, &icuStatus);

if (U_FAILURE(icuStatus) != U_ZERO_ERROR) {
return -1;
}

for (int i = 0; i < icuValueLength; i++) {
icuValue.push_back(buffer[i]);
}

memcpy(currency, icuValue.c_str(), icuValue.length() + 1);

return icuValue.length();
}

int ecma402_getHourCycle(const char *localeId, char *hourCycle,
ecma402_errorStatus *status, bool isCanonicalized) {
return getKeywordValue(ICU_KEYWORD_HOUR_CYCLE, localeId, hourCycle, status,
Expand Down Expand Up @@ -371,6 +439,7 @@ ecma402_locale *ecma402_initEmptyLocale(void) {
locale->canonical = nullptr;
locale->caseFirst = nullptr;
locale->collation = nullptr;
locale->currency = nullptr;
locale->hourCycle = nullptr;
locale->language = nullptr;
locale->numberingSystem = nullptr;
Expand Down Expand Up @@ -412,6 +481,7 @@ ecma402_locale *ecma402_initLocale(const char *localeId) {
INIT_PROPERTY(canonical, calendar, ULOC_KEYWORDS_CAPACITY, getCalendar);
INIT_PROPERTY(canonical, caseFirst, ULOC_KEYWORDS_CAPACITY, getCaseFirst);
INIT_PROPERTY(canonical, collation, ULOC_KEYWORDS_CAPACITY, getCollation);
INIT_PROPERTY(canonical, currency, 4, getCurrency);
INIT_PROPERTY(canonical, hourCycle, ULOC_KEYWORDS_CAPACITY, getHourCycle);
INIT_PROPERTY(canonical, language, ULOC_LANG_CAPACITY, getLanguage);
INIT_PROPERTY(canonical, numberingSystem, ULOC_KEYWORDS_CAPACITY,
Expand Down Expand Up @@ -468,8 +538,7 @@ int ecma402_keywordsOfLocale(ecma402_locale *locale, const char *keyword,

canonical = locale->canonical;

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

if (preferredLength > 0) {
// If the keyword is "currency," there's some special handling: it must have
// a length of exactly 3, and it must not have a value of "YES" (which
// implies the length was actually less than 3 and ICU converted that to a
// truthy string "YES").
// If the keyword is not "currency," then the length must be greater than 0.
if ((strcmp(keyword, ICU_KEYWORD_CURRENCY) == 0 && preferredLength == 3 &&
strcasecmp(preferred, "YES") != 0) ||
(strcmp(keyword, ICU_KEYWORD_CURRENCY) != 0 && preferredLength > 0)) {
values[0] = strdup(uloc_toUnicodeLocaleType(keyword, preferred));
free(preferred);
return 1;
Expand Down
15 changes: 12 additions & 3 deletions src/ecma402/locale.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ typedef struct ecma402_locale {
*/
char *collation;

/**
* The currency (cu) property of the locale identifier, if available.
*/
char *currency;

/**
* The hours (hc) property of the locale identifier, if available.
*/
Expand Down Expand Up @@ -107,6 +112,7 @@ typedef struct ecma402_locale {
* @param calendar The calendar (ca) type to set; use NULL to ignore.
* @param caseFirst The colcasefirst (kf) type to set; use NULL to ignore.
* @param collation The collation (co) type to set; use NULL to ignore.
* @param currency The currency (cu) type to set; use NULL to ignore.
* @param hourCycle The hours (hc) type to set; use NULL to ignore.
* @param language The language type to set; use NULL to ignore.
* @param numberingSystem The numbers (nu) type to set; use NULL to ignore.
Expand All @@ -117,9 +123,9 @@ typedef struct ecma402_locale {
ecma402_locale *
ecma402_applyLocaleOptions(ecma402_locale *locale, const char *calendar,
const char *caseFirst, const char *collation,
const char *hourCycle, const char *language,
const char *numberingSystem, int numeric,
const char *region, const char *script);
const char *currency, const char *hourCycle,
const char *language, const char *numberingSystem,
int numeric, const char *region, const char *script);

/**
* Canonicalizes a list of locales.
Expand Down Expand Up @@ -256,6 +262,9 @@ int ecma402_getCaseFirst(const char *localeId, char *caseFirst,
int ecma402_getCollation(const char *localeId, char *collation,
ecma402_errorStatus *status, bool isCanonicalized);

int ecma402_getCurrency(const char *localeId, char *currency,
ecma402_errorStatus *status, bool isCanonicalized);

/**
* Returns the value of the hours (hc) keyword for the given locale ID.
*
Expand Down
7 changes: 5 additions & 2 deletions src/php/classes/locale.c
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ PHP_METHOD(Ecma_Intl_Locale, __construct) {
SET_PROPERTY_STRING(collation);
SET_PROPERTY_ARRAY(collations, ECMA402_LOCALE_COLLATION_CAPACITY);
SET_PROPERTY_ARRAY(currencies, ECMA402_LOCALE_CURRENCY_CAPACITY);
SET_PROPERTY_STRING(currency);
SET_PROPERTY_STRING(hourCycle);
SET_PROPERTY_ARRAY(hourCycles, ECMA402_LOCALE_HOUR_CYCLE_CAPACITY);
SET_PROPERTY_STRING(language);
Expand Down Expand Up @@ -223,6 +224,7 @@ PHP_METHOD(Ecma_Intl_Locale, jsonSerialize) {
ADD_TO_JSON(collation);
ADD_TO_JSON(collations);
ADD_TO_JSON(currencies);
ADD_TO_JSON(currency);
ADD_TO_JSON(hourCycle);
ADD_TO_JSON(hourCycles);
ADD_TO_JSON(language);
Expand Down Expand Up @@ -276,6 +278,7 @@ static ecma402_locale *applyOptions(ecma402_locale *locale,
const char *calendar = getOption(options, "calendar");
const char *caseFirst = getOption(options, "caseFirst");
const char *collation = getOption(options, "collation");
const char *currency = getOption(options, "currency");
const char *hourCycle = getOption(options, "hourCycle");
const char *language = getOption(options, "language");
const char *numberingSystem = getOption(options, "numberingSystem");
Expand All @@ -284,8 +287,8 @@ static ecma402_locale *applyOptions(ecma402_locale *locale,
const char *script = getOption(options, "script");

return ecma402_applyLocaleOptions(locale, calendar, caseFirst, collation,
hourCycle, language, numberingSystem,
numeric, region, script);
currency, hourCycle, language,
numberingSystem, numeric, region, script);
}

static void freeLocaleObj(zend_object *object) {
Expand Down
23 changes: 21 additions & 2 deletions src/php/classes/locale.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,21 @@
*/
public readonly array $currencies;

/**
* The `currency` property has the currency type for this locale.
*
* The currency type is the 3-character ISO 4217 currency code. If
* neither the `cu` key of the locale identifier nor the `currency`
* property of the {@see Locale\Options} is set, this value is `null`.
*
* This property is not defined in ECMA-402 or in the Intl Locale Info
* Proposal. Instead, this is unique to the PHP implementation and is
* inspired by the Intl Locale Info Proposal.
*
* @link https://tc39.es/proposal-intl-locale-info/ Intl Locale Info Proposal
*/
public readonly ?string $currency;

/**
* The `hourCycle` property has the hour cycle type for this locale.
*
Expand Down Expand Up @@ -264,10 +279,14 @@ public function getCollations(): array
* Returns a list of one or more currency types commonly used for this
* locale.
*
* If the locale already includes a currency (e.g., `en-u-cu-eur`) or
* one was provided via the constructor's `$options` parameter, this
* list will contain only that currency type.
*
* This method is not defined in ECMA-402 or in the Intl Locale Info
* Proposal in which other similar methods are described. Instead, this
* is unique to the PHP implementation and draws its inspiration from
* the Intl Locale Info Proposal.
* is unique to the PHP implementation and is inspired by the Intl
* Locale Info Proposal.
*
* @link https://tc39.es/proposal-intl-locale-info/ Intl Locale Info Proposal
* @link https://github.com/tc39/proposal-intl-locale-info/issues/75 Possible of addition of Intl.Locale.prototype.getCurrencies()?
Expand Down
Loading