From a701ed52fc713b9d8403e8fbdc6038eef3e8759a Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Fri, 12 Nov 2021 23:16:37 -0500 Subject: [PATCH] LibJS+LibUnicode: Fully implement currency number formatting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currencies are a bit strange; the layout of currency data in the CLDR is not particularly compatible with what ECMA-402 expects. For example, the currency format in the "en" and "ar" locales for the Latin script are: en: "¤#,##0.00" ar: "¤\u00A0#,##0.00" Note how the "ar" locale has a non-breaking space after the currency symbol (¤), but "en" does not. This does not mean that this space will appear in the "ar"-formatted string, nor does it mean that a space won't appear in the "en"-formatted string. This is a runtime decision based on the currency display chosen by the user ("$" vs. "USD" vs. "US dollar") and other rules in the Unicode TR-35 spec. ECMA-402 shies away from the nuances here with "implementation-defined" steps. LibUnicode will store the data parsed from the CLDR however it is presented; making decisions about spacing, etc. will occur at runtime based on user input. --- .../GenerateUnicodeNumberFormat.cpp | 2 +- .../LibJS/Runtime/Intl/NumberFormat.cpp | 76 ++-- .../LibJS/Runtime/Intl/NumberFormat.h | 2 +- .../NumberFormat.prototype.format.js | 326 ++++++++++++++++++ Userland/Libraries/LibUnicode/Locale.cpp | 75 ++++ Userland/Libraries/LibUnicode/Locale.h | 3 + 6 files changed, 454 insertions(+), 30 deletions(-) diff --git a/Meta/Lagom/Tools/CodeGenerators/LibUnicode/GenerateUnicodeNumberFormat.cpp b/Meta/Lagom/Tools/CodeGenerators/LibUnicode/GenerateUnicodeNumberFormat.cpp index dcebeb0c9a..fb19c3ae95 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibUnicode/GenerateUnicodeNumberFormat.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibUnicode/GenerateUnicodeNumberFormat.cpp @@ -94,7 +94,7 @@ static void parse_number_pattern(String pattern, UnicodeLocaleData& locale_data, { "%"sv, "{percentSign}"sv }, { "+"sv, "{plusSign}"sv }, { "-"sv, "{minusSign}"sv }, - { "¤"sv, "{currencyCode}"sv }, // U+00A4 Currency Sign + { "¤"sv, "{currency}"sv }, // U+00A4 Currency Sign }; if (auto start_number_index = pattern.find_any_of("#0"sv, String::SearchDirection::Forward); start_number_index.has_value()) { diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp index 508d3e7b3b..ebe54f9e20 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp @@ -5,7 +5,6 @@ */ #include -#include #include #include #include @@ -596,7 +595,7 @@ Vector partition_number_pattern(NumberFormat& number_format, d Vector result; // 7. Let patternParts be PartitionPattern(pattern). - auto pattern_parts = partition_pattern(*pattern); + auto pattern_parts = pattern->visit([](auto const& p) { return partition_pattern(p); }); // 8. For each Record { [[Type]], [[Value]] } patternPart of patternParts, do for (auto& pattern_part : pattern_parts) { @@ -662,32 +661,11 @@ Vector partition_number_pattern(NumberFormat& number_format, d } // i. Else if p is equal to "currencyCode" and numberFormat.[[Style]] is "currency", then - else if ((part == "currencyCode"sv) && (number_format.style() == NumberFormat::Style::Currency)) { - // i. Let currency be numberFormat.[[Currency]]. - // ii. Let cd be currency. - // iii. Append a new Record { [[Type]]: "currency", [[Value]]: cd } as the last element of result. - result.append({ part, number_format.currency() }); - } - // j. Else if p is equal to "currencyPrefix" and numberFormat.[[Style]] is "currency", then - else if ((part == "currencyPrefix"sv) && (number_format.style() == NumberFormat::Style::Currency)) { - // i. Let currency be numberFormat.[[Currency]]. - // ii. Let currencyDisplay be numberFormat.[[CurrencyDisplay]]. - // iii. Let cd be an ILD String value representing currency before x in currencyDisplay form, which may depend on x in languages having different plural forms. - // iv. Append a new Record { [[Type]]: "currency", [[Value]]: cd } as the last element of result. - - // FIXME: LibUnicode will need to parse currencies.json and the "currencySpacing/beforeCurrency" object from numbers.json. - } - // k. Else if p is equal to "currencySuffix" and numberFormat.[[Style]] is "currency", then - else if ((part == "currencySuffix"sv) && (number_format.style() == NumberFormat::Style::Currency)) { - // i. Let currency be numberFormat.[[Currency]]. - // ii. Let currencyDisplay be numberFormat.[[CurrencyDisplay]]. - // iii. Let cd be an ILD String value representing currency after x in currencyDisplay form, which may depend on x in languages having different plural forms. If the implementation does not have such a representation of currency, use currency itself. - // iv. Append a new Record { [[Type]]: "currency", [[Value]]: cd } as the last element of result. - - // FIXME: LibUnicode will need to parse currencies.json and the "currencySpacing/afterCurrency" object from numbers.json. - } + // + // Note: Our implementation formats currency codes during GetNumberFormatPattern so that we + // do not have to do currency display / plurality lookups more than once. // l. Else, else { @@ -1198,7 +1176,7 @@ ThrowCompletionOr set_number_format_unit_options(GlobalObject& global_obje } // 15.1.14 GetNumberFormatPattern ( numberFormat, x ), https://tc39.es/ecma402/#sec-getnumberformatpattern -Optional get_number_format_pattern(NumberFormat& number_format, double number) +Optional> get_number_format_pattern(NumberFormat& number_format, double number) { // 1. Let localeData be %NumberFormat%.[[LocaleData]]. // 2. Let dataLocale be numberFormat.[[DataLocale]]. @@ -1239,11 +1217,20 @@ Optional get_number_format_pattern(NumberFormat& number_format, doub // f. Let patterns be patterns.[[]]. // g. Let patterns be patterns.[[]]. // h. Let patterns be patterns.[[]]. + + // Handling of other [[CurrencyDisplay]] options will occur after [[SignDisplay]]. + if (number_format.currency_display() == NumberFormat::CurrencyDisplay::Name) { + auto maybe_patterns = Unicode::select_currency_unit_pattern(number_format.data_locale(), number_format.numbering_system(), number); + if (maybe_patterns.has_value()) { + patterns = maybe_patterns.release_value(); + break; + } + } + switch (number_format.currency_sign()) { case NumberFormat::CurrencySign::Standard: patterns = Unicode::get_standard_number_system_format(number_format.data_locale(), number_format.numbering_system(), Unicode::StandardNumberFormatType::Currency); break; - case NumberFormat::CurrencySign::Accounting: patterns = Unicode::get_standard_number_system_format(number_format.data_locale(), number_format.numbering_system(), Unicode::StandardNumberFormatType::Accounting); break; @@ -1332,6 +1319,39 @@ Optional get_number_format_pattern(NumberFormat& number_format, doub VERIFY_NOT_REACHED(); } + // Handling of steps 9b/9g: Depending on the currency display and the format pattern found above, + // we might need to mutate the format pattern to inject a space between the currency display and + // the currency number. + if (number_format.style() == NumberFormat::Style::Currency) { + if (number_format.currency_display() == NumberFormat::CurrencyDisplay::Name) { + auto maybe_currency_display = Unicode::get_locale_currency_mapping(number_format.data_locale(), number_format.currency(), Unicode::Style::Numeric); + auto currency_display = maybe_currency_display.value_or(number_format.currency()); + + return pattern.replace("{0}"sv, "{number}"sv).replace("{1}"sv, currency_display); + } + + Optional currency_display; + + switch (number_format.currency_display()) { + case NumberFormat::CurrencyDisplay::Code: + currency_display = number_format.currency(); + break; + case NumberFormat::CurrencyDisplay::Symbol: + currency_display = Unicode::get_locale_currency_mapping(number_format.data_locale(), number_format.currency(), Unicode::Style::Short); + break; + case NumberFormat::CurrencyDisplay::NarrowSymbol: + currency_display = Unicode::get_locale_currency_mapping(number_format.data_locale(), number_format.currency(), Unicode::Style::Narrow); + break; + default: + VERIFY_NOT_REACHED(); + } + + if (!currency_display.has_value()) + currency_display = number_format.currency(); + + return Unicode::create_currency_format_pattern(*currency_display, pattern); + } + // 16. Return pattern. return pattern; } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h index 5dc45dc88d..32719d63a3 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h @@ -198,7 +198,7 @@ String format_numeric(NumberFormat& number_format, double number); RawFormatResult to_raw_precision(double number, int min_precision, int max_precision); RawFormatResult to_raw_fixed(double number, int min_fraction, int max_fraction); ThrowCompletionOr set_number_format_unit_options(GlobalObject& global_object, NumberFormat& intl_object, Object const& options); -Optional get_number_format_pattern(NumberFormat& number_format, double number); +Optional> get_number_format_pattern(NumberFormat& number_format, double number); StringView get_notation_sub_pattern(NumberFormat& number_format, int exponent); int compute_exponent(NumberFormat& number_format, double number); int compute_exponent_for_magniude(NumberFormat& number_format, int magnitude); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.format.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.format.js index 5f0c89a425..7e388c5f83 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.format.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.format.js @@ -355,3 +355,329 @@ describe("style=percent", () => { expect(ar.format(-0.01)).toBe("\u061c-\u0661\u066a\u061c"); }); }); + +describe("style=currency", () => { + test("currencyDisplay=code", () => { + const en1 = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + currencyDisplay: "code", + }); + expect(en1.format(1)).toBe("USD\u00a01.00"); + expect(en1.format(1.2)).toBe("USD\u00a01.20"); + expect(en1.format(1.23)).toBe("USD\u00a01.23"); + + const en2 = new Intl.NumberFormat("en", { + style: "currency", + currency: "KHR", + currencyDisplay: "code", + }); + expect(en2.format(1)).toBe("KHR\u00a01.00"); + expect(en2.format(1.2)).toBe("KHR\u00a01.20"); + expect(en2.format(1.23)).toBe("KHR\u00a01.23"); + + const ar1 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "code", + }); + expect(ar1.format(1)).toBe("\u0661\u066b\u0660\u0660\u00a0USD"); + expect(ar1.format(1.2)).toBe("\u0661\u066b\u0662\u0660\u00a0USD"); + expect(ar1.format(1.23)).toBe("\u0661\u066b\u0662\u0663\u00a0USD"); + + const ar2 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "code", + numberingSystem: "latn", + }); + expect(ar2.format(1)).toBe("USD\u00a01.00"); + expect(ar2.format(1.2)).toBe("USD\u00a01.20"); + expect(ar2.format(1.23)).toBe("USD\u00a01.23"); + }); + + test("currencyDisplay=symbol", () => { + const en1 = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + currencyDisplay: "symbol", + }); + expect(en1.format(1)).toBe("$1.00"); + expect(en1.format(1.2)).toBe("$1.20"); + expect(en1.format(1.23)).toBe("$1.23"); + + const en2 = new Intl.NumberFormat("en", { + style: "currency", + currency: "KHR", + currencyDisplay: "symbol", + }); + expect(en2.format(1)).toBe("KHR\u00a01.00"); + expect(en2.format(1.2)).toBe("KHR\u00a01.20"); + expect(en2.format(1.23)).toBe("KHR\u00a01.23"); + + const ar1 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "symbol", + }); + expect(ar1.format(1)).toBe("\u0661\u066b\u0660\u0660\u00a0US$"); + expect(ar1.format(1.2)).toBe("\u0661\u066b\u0662\u0660\u00a0US$"); + expect(ar1.format(1.23)).toBe("\u0661\u066b\u0662\u0663\u00a0US$"); + + const ar2 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "symbol", + numberingSystem: "latn", + }); + expect(ar2.format(1)).toBe("US$\u00a01.00"); + expect(ar2.format(1.2)).toBe("US$\u00a01.20"); + expect(ar2.format(1.23)).toBe("US$\u00a01.23"); + }); + + test("currencyDisplay=narrowSymbol", () => { + const en1 = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + currencyDisplay: "narrowSymbol", + }); + expect(en1.format(1)).toBe("$1.00"); + expect(en1.format(1.2)).toBe("$1.20"); + expect(en1.format(1.23)).toBe("$1.23"); + + const en2 = new Intl.NumberFormat("en", { + style: "currency", + currency: "KHR", + currencyDisplay: "narrowSymbol", + }); + expect(en2.format(1)).toBe("៛1.00"); + expect(en2.format(1.2)).toBe("៛1.20"); + expect(en2.format(1.23)).toBe("៛1.23"); + + const ar1 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "narrowSymbol", + }); + expect(ar1.format(1)).toBe("\u0661\u066b\u0660\u0660\u00a0US$"); + expect(ar1.format(1.2)).toBe("\u0661\u066b\u0662\u0660\u00a0US$"); + expect(ar1.format(1.23)).toBe("\u0661\u066b\u0662\u0663\u00a0US$"); + + const ar2 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "narrowSymbol", + numberingSystem: "latn", + }); + expect(ar2.format(1)).toBe("US$\u00a01.00"); + expect(ar2.format(1.2)).toBe("US$\u00a01.20"); + expect(ar2.format(1.23)).toBe("US$\u00a01.23"); + }); + + test("currencyDisplay=name", () => { + const en1 = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + currencyDisplay: "name", + }); + expect(en1.format(1)).toBe("1.00 US dollars"); + expect(en1.format(1.2)).toBe("1.20 US dollars"); + expect(en1.format(1.23)).toBe("1.23 US dollars"); + + const en2 = new Intl.NumberFormat("en", { + style: "currency", + currency: "KHR", + currencyDisplay: "name", + }); + expect(en2.format(1)).toBe("1.00 Cambodian riels"); + expect(en2.format(1.2)).toBe("1.20 Cambodian riels"); + expect(en2.format(1.23)).toBe("1.23 Cambodian riels"); + + const ar1 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "name", + }); + expect(ar1.format(1)).toBe("\u0661\u066b\u0660\u0660 دولار أمريكي"); + expect(ar1.format(1.2)).toBe("\u0661\u066b\u0662\u0660 دولار أمريكي"); + expect(ar1.format(1.23)).toBe("\u0661\u066b\u0662\u0663 دولار أمريكي"); + + const ar2 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencyDisplay: "name", + numberingSystem: "latn", + }); + expect(ar2.format(1)).toBe("1.00 دولار أمريكي"); + expect(ar2.format(1.2)).toBe("1.20 دولار أمريكي"); + expect(ar2.format(1.23)).toBe("1.23 دولار أمريكي"); + }); + + test("signDisplay=never", () => { + const en1 = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + signDisplay: "never", + }); + expect(en1.format(1)).toBe("$1.00"); + expect(en1.format(-1)).toBe("$1.00"); + + const en2 = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + currencySign: "accounting", + signDisplay: "never", + }); + expect(en2.format(1)).toBe("$1.00"); + expect(en2.format(-1)).toBe("$1.00"); + + const ar1 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + signDisplay: "never", + }); + expect(ar1.format(1)).toBe("\u0661\u066b\u0660\u0660\u00a0US$"); + expect(ar1.format(-1)).toBe("\u0661\u066b\u0660\u0660\u00a0US$"); + + const ar2 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencySign: "accounting", + signDisplay: "never", + }); + expect(ar2.format(1)).toBe("\u0661\u066b\u0660\u0660\u00a0US$"); + expect(ar2.format(-1)).toBe("\u0661\u066b\u0660\u0660\u00a0US$"); + }); + + test("signDisplay=auto", () => { + const en1 = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + signDisplay: "auto", + }); + expect(en1.format(0)).toBe("$0.00"); + expect(en1.format(1)).toBe("$1.00"); + expect(en1.format(-0)).toBe("-$0.00"); + expect(en1.format(-1)).toBe("-$1.00"); + + const en2 = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + currencySign: "accounting", + signDisplay: "auto", + }); + expect(en2.format(0)).toBe("$0.00"); + expect(en2.format(1)).toBe("$1.00"); + expect(en2.format(-0)).toBe("($0.00)"); + expect(en2.format(-1)).toBe("($1.00)"); + + const ar1 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + signDisplay: "auto", + }); + expect(ar1.format(0)).toBe("\u0660\u066b\u0660\u0660\u00a0US$"); + expect(ar1.format(1)).toBe("\u0661\u066b\u0660\u0660\u00a0US$"); + expect(ar1.format(-0)).toBe("\u061c-\u0660\u066b\u0660\u0660\u00a0US$"); + expect(ar1.format(-1)).toBe("\u061c-\u0661\u066b\u0660\u0660\u00a0US$"); + + const ar2 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencySign: "accounting", + signDisplay: "auto", + }); + expect(ar2.format(0)).toBe("\u0660\u066b\u0660\u0660\u00a0US$"); + expect(ar2.format(1)).toBe("\u0661\u066b\u0660\u0660\u00a0US$"); + expect(ar2.format(-0)).toBe("\u061c-\u0660\u066b\u0660\u0660\u00a0US$"); + expect(ar2.format(-1)).toBe("\u061c-\u0661\u066b\u0660\u0660\u00a0US$"); + }); + + test("signDisplay=always", () => { + const en1 = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + signDisplay: "always", + }); + expect(en1.format(0)).toBe("+$0.00"); + expect(en1.format(1)).toBe("+$1.00"); + expect(en1.format(-0)).toBe("-$0.00"); + expect(en1.format(-1)).toBe("-$1.00"); + + const en2 = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + currencySign: "accounting", + signDisplay: "always", + }); + expect(en2.format(0)).toBe("+$0.00"); + expect(en2.format(1)).toBe("+$1.00"); + expect(en2.format(-0)).toBe("($0.00)"); + expect(en2.format(-1)).toBe("($1.00)"); + + const ar1 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + signDisplay: "always", + }); + expect(ar1.format(0)).toBe("\u061c+\u0660\u066b\u0660\u0660\u00a0US$"); + expect(ar1.format(1)).toBe("\u061c+\u0661\u066b\u0660\u0660\u00a0US$"); + expect(ar1.format(-0)).toBe("\u061c-\u0660\u066b\u0660\u0660\u00a0US$"); + expect(ar1.format(-1)).toBe("\u061c-\u0661\u066b\u0660\u0660\u00a0US$"); + + const ar2 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencySign: "accounting", + signDisplay: "always", + }); + expect(ar2.format(0)).toBe("\u061c+\u0660\u066b\u0660\u0660\u00a0US$"); + expect(ar2.format(1)).toBe("\u061c+\u0661\u066b\u0660\u0660\u00a0US$"); + expect(ar2.format(-0)).toBe("\u061c-\u0660\u066b\u0660\u0660\u00a0US$"); + expect(ar2.format(-1)).toBe("\u061c-\u0661\u066b\u0660\u0660\u00a0US$"); + }); + + test("signDisplay=exceptZero", () => { + const en1 = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + signDisplay: "exceptZero", + }); + expect(en1.format(0)).toBe("$0.00"); + expect(en1.format(1)).toBe("+$1.00"); + expect(en1.format(-0)).toBe("$0.00"); + expect(en1.format(-1)).toBe("-$1.00"); + + const en2 = new Intl.NumberFormat("en", { + style: "currency", + currency: "USD", + currencySign: "accounting", + signDisplay: "exceptZero", + }); + expect(en2.format(0)).toBe("$0.00"); + expect(en2.format(1)).toBe("+$1.00"); + expect(en2.format(-0)).toBe("$0.00"); + expect(en2.format(-1)).toBe("($1.00)"); + + const ar1 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + signDisplay: "exceptZero", + }); + expect(ar1.format(0)).toBe("\u0660\u066b\u0660\u0660\u00a0US$"); + expect(ar1.format(1)).toBe("\u061c+\u0661\u066b\u0660\u0660\u00a0US$"); + expect(ar1.format(-0)).toBe("\u0660\u066b\u0660\u0660\u00a0US$"); + expect(ar1.format(-1)).toBe("\u061c-\u0661\u066b\u0660\u0660\u00a0US$"); + + const ar2 = new Intl.NumberFormat("ar", { + style: "currency", + currency: "USD", + currencySign: "accounting", + signDisplay: "exceptZero", + }); + expect(ar2.format(0)).toBe("\u0660\u066b\u0660\u0660\u00a0US$"); + expect(ar2.format(1)).toBe("\u061c+\u0661\u066b\u0660\u0660\u00a0US$"); + expect(ar2.format(-0)).toBe("\u0660\u066b\u0660\u0660\u00a0US$"); + expect(ar2.format(-1)).toBe("\u061c-\u0661\u066b\u0660\u0660\u00a0US$"); + }); +}); diff --git a/Userland/Libraries/LibUnicode/Locale.cpp b/Userland/Libraries/LibUnicode/Locale.cpp index 92cbd88f88..7e384e2e62 100644 --- a/Userland/Libraries/LibUnicode/Locale.cpp +++ b/Userland/Libraries/LibUnicode/Locale.cpp @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include #if ENABLE_UNICODE_DATA @@ -963,6 +965,79 @@ String resolve_most_likely_territory([[maybe_unused]] LanguageID const& language return aliases[0].to_string(); } +Optional select_currency_unit_pattern(StringView locale, StringView system, double number) +{ + // FIXME: This is a rather naive and locale-unaware implementation Unicode's TR-35 pluralization + // rules: https://www.unicode.org/reports/tr35/tr35-numbers.html#Language_Plural_Rules + // Once those rules are implemented for LibJS, we better use them instead. + auto formats = get_compact_number_system_formats(locale, system, CompactNumberFormatType::CurrencyUnit); + + auto find_plurality = [&](auto plurality) -> Optional { + if (auto it = formats.find_if([&](auto& patterns) { return patterns.plurality == plurality; }); it != formats.end()) + return *it; + return {}; + }; + + if (number == 0) { + if (auto patterns = find_plurality(NumberFormat::Plurality::Zero); patterns.has_value()) + return patterns; + } else if (number == 1) { + if (auto patterns = find_plurality(NumberFormat::Plurality::One); patterns.has_value()) + return patterns; + } else if (number == 2) { + if (auto patterns = find_plurality(NumberFormat::Plurality::Two); patterns.has_value()) + return patterns; + } else { + if (auto patterns = find_plurality(NumberFormat::Plurality::Many); patterns.has_value()) + return patterns; + } + + return find_plurality(NumberFormat::Plurality::Other); +} + +// https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies +String create_currency_format_pattern(StringView currency_display, StringView base_pattern) +{ + constexpr auto number_key = "{number}"sv; + constexpr auto currency_key = "{currency}"sv; + constexpr auto spacing = "\u00A0"sv; // No-Break Space (NBSP) + + auto number_index = base_pattern.find(number_key); + VERIFY(number_index.has_value()); + + auto currency_index = base_pattern.find(currency_key); + VERIFY(currency_index.has_value()); + + static auto symbol_category = general_category_from_string("Symbol"sv); + VERIFY(symbol_category.has_value()); // This shouldn't be reached if Unicode generation is disabled. + + Utf8View utf8_currency_display { currency_display }; + Optional currency_display_with_spacing; + + if (*number_index < *currency_index) { + if (!base_pattern.substring_view(0, *currency_index).ends_with(spacing)) { + u32 first_currency_code_point = *utf8_currency_display.begin(); + + if (!code_point_has_general_category(first_currency_code_point, *symbol_category)) + currency_display_with_spacing = String::formatted("{}{}", spacing, currency_display); + } + } else { + if (!base_pattern.substring_view(0, *number_index).ends_with(spacing)) { + u32 last_currency_code_point = 0; + for (auto it = utf8_currency_display.begin(); it != utf8_currency_display.end(); ++it) + last_currency_code_point = *it; + + if (!code_point_has_general_category(last_currency_code_point, *symbol_category)) + currency_display_with_spacing = String::formatted("{}{}", currency_display, spacing); + } + } + + if (currency_display_with_spacing.has_value()) + return base_pattern.replace(currency_key, *currency_display_with_spacing); + + return base_pattern.replace(currency_key, currency_display); +} + String LanguageID::to_string() const { StringBuilder builder; diff --git a/Userland/Libraries/LibUnicode/Locale.h b/Userland/Libraries/LibUnicode/Locale.h index 40f1c47317..f205739a81 100644 --- a/Userland/Libraries/LibUnicode/Locale.h +++ b/Userland/Libraries/LibUnicode/Locale.h @@ -195,4 +195,7 @@ Optional add_likely_subtags(LanguageID const& language_id); Optional remove_likely_subtags(LanguageID const& language_id); String resolve_most_likely_territory(LanguageID const& language_id, StringView territory_alias); +Optional select_currency_unit_pattern(StringView locale, StringView system, double number); +String create_currency_format_pattern(StringView currency_display, StringView base_pattern); + }