浏览代码

LibJS+LibUnicode: Fully implement currency number formatting

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.
Timothy Flynn 3 年之前
父节点
当前提交
a701ed52fc

+ 1 - 1
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()) {

+ 48 - 28
Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp

@@ -5,7 +5,6 @@
  */
 
 #include <AK/Array.h>
-#include <AK/Utf8View.h>
 #include <LibJS/Runtime/GlobalObject.h>
 #include <LibJS/Runtime/Intl/NumberFormat.h>
 #include <LibJS/Runtime/Intl/NumberFormatFunction.h>
@@ -596,7 +595,7 @@ Vector<PatternPartition> partition_number_pattern(NumberFormat& number_format, d
     Vector<PatternPartition> 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<PatternPartition> 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<void> set_number_format_unit_options(GlobalObject& global_obje
 }
 
 // 15.1.14 GetNumberFormatPattern ( numberFormat, x ), https://tc39.es/ecma402/#sec-getnumberformatpattern
-Optional<StringView> get_number_format_pattern(NumberFormat& number_format, double number)
+Optional<Variant<StringView, String>> 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<StringView> get_number_format_pattern(NumberFormat& number_format, doub
         // f. Let patterns be patterns.[[<currency>]].
         // g. Let patterns be patterns.[[<currencyDisplay>]].
         // h. Let patterns be patterns.[[<currencySign>]].
+
+        // 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<StringView> 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<StringView> 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;
 }

+ 1 - 1
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<void> set_number_format_unit_options(GlobalObject& global_object, NumberFormat& intl_object, Object const& options);
-Optional<StringView> get_number_format_pattern(NumberFormat& number_format, double number);
+Optional<Variant<StringView, String>> 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);

+ 326 - 0
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$");
+    });
+});

+ 75 - 0
Userland/Libraries/LibUnicode/Locale.cpp

@@ -8,6 +8,8 @@
 #include <AK/GenericLexer.h>
 #include <AK/QuickSort.h>
 #include <AK/StringBuilder.h>
+#include <AK/Utf8View.h>
+#include <LibUnicode/CharacterTypes.h>
 #include <LibUnicode/Locale.h>
 
 #if ENABLE_UNICODE_DATA
@@ -963,6 +965,79 @@ String resolve_most_likely_territory([[maybe_unused]] LanguageID const& language
     return aliases[0].to_string();
 }
 
+Optional<NumberFormat> 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<NumberFormat> {
+        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<String> 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;

+ 3 - 0
Userland/Libraries/LibUnicode/Locale.h

@@ -195,4 +195,7 @@ Optional<LanguageID> add_likely_subtags(LanguageID const& language_id);
 Optional<LanguageID> remove_likely_subtags(LanguageID const& language_id);
 String resolve_most_likely_territory(LanguageID const& language_id, StringView territory_alias);
 
+Optional<NumberFormat> select_currency_unit_pattern(StringView locale, StringView system, double number);
+String create_currency_format_pattern(StringView currency_display, StringView base_pattern);
+
 }