From 67f3de23201be5d3da174b7b68b4cec129c5fbd3 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sun, 9 Jun 2024 14:36:48 -0400 Subject: [PATCH] LibJS+LibLocale: Begin replacing number formatting with ICU This uses ICU for the Intl.NumberFormat `format` and `formatToParts` prototypes. It does not yet port the range formatter prototypes. Most of the new code in LibLocale/NumberFormat is simply mapping from ECMA-402 types to ICU types. Beyond that, the only algorithmic change is that we have to mutate the output from ICU for `formatToParts` to match what is expected by ECMA-402. This is explained in NumberFormat.cpp in `flatten_partitions`. This lets us remove most data from our number format generator. All that remains are numbering system digits and symbols, which are relied upon still for other interfaces (e.g. Intl.DateTimeFormat). So they will be removed in a future patch. Note: All of the changes to the test files in this patch are now aligned with both Chrome and Safari. --- Meta/CMake/locale_data.cmake | 6 +- .../LibLocale/GenerateNumberFormatData.cpp | 706 +------ .../LibJS/Runtime/BigIntPrototype.cpp | 2 +- .../LibJS/Runtime/Intl/AbstractOperations.h | 3 +- .../LibJS/Runtime/Intl/DateTimeFormat.cpp | 6 +- .../LibJS/Runtime/Intl/DurationFormat.cpp | 4 +- .../LibJS/Runtime/Intl/MathematicalValue.cpp | 263 +-- .../LibJS/Runtime/Intl/MathematicalValue.h | 36 +- .../LibJS/Runtime/Intl/NumberFormat.cpp | 1652 +---------------- .../LibJS/Runtime/Intl/NumberFormat.h | 226 +-- .../Runtime/Intl/NumberFormatConstructor.cpp | 40 +- .../Runtime/Intl/NumberFormatConstructor.h | 4 +- .../Runtime/Intl/NumberFormatFunction.cpp | 4 +- .../LibJS/Runtime/Intl/PluralRules.cpp | 15 +- .../LibJS/Runtime/Intl/PluralRules.h | 3 +- .../Runtime/Intl/PluralRulesConstructor.cpp | 12 +- .../LibJS/Runtime/Intl/RelativeTimeFormat.cpp | 6 +- .../LibJS/Runtime/Intl/RelativeTimeFormat.h | 5 +- .../LibJS/Runtime/NumberPrototype.cpp | 2 +- .../NumberFormat.prototype.format.js | 46 +- .../NumberFormat.prototype.formatToParts.js | 150 +- Userland/Libraries/LibLocale/Forward.h | 6 +- Userland/Libraries/LibLocale/NumberFormat.cpp | 768 +++++++- Userland/Libraries/LibLocale/NumberFormat.h | 169 +- 24 files changed, 1168 insertions(+), 2966 deletions(-) diff --git a/Meta/CMake/locale_data.cmake b/Meta/CMake/locale_data.cmake index ec3ce56c1b4..1d907046b75 100644 --- a/Meta/CMake/locale_data.cmake +++ b/Meta/CMake/locale_data.cmake @@ -24,9 +24,6 @@ set(CLDR_LOCALES_PATH "${CLDR_PATH}/${CLDR_LOCALES_SOURCE}") set(CLDR_NUMBERS_SOURCE cldr-numbers-modern) set(CLDR_NUMBERS_PATH "${CLDR_PATH}/${CLDR_NUMBERS_SOURCE}") -set(CLDR_UNITS_SOURCE cldr-units-modern) -set(CLDR_UNITS_PATH "${CLDR_PATH}/${CLDR_UNITS_SOURCE}") - if (ENABLE_UNICODE_DATABASE_DOWNLOAD) remove_path_if_version_changed("${CLDR_VERSION}" "${CLDR_VERSION_FILE}" "${CLDR_PATH}") @@ -37,7 +34,6 @@ if (ENABLE_UNICODE_DATABASE_DOWNLOAD) extract_path("${CLDR_PATH}" "${CLDR_ZIP_PATH}" "${CLDR_DATES_SOURCE}/**" "${CLDR_DATES_PATH}") extract_path("${CLDR_PATH}" "${CLDR_ZIP_PATH}" "${CLDR_LOCALES_SOURCE}/**" "${CLDR_LOCALES_PATH}") extract_path("${CLDR_PATH}" "${CLDR_ZIP_PATH}" "${CLDR_NUMBERS_SOURCE}/**" "${CLDR_NUMBERS_PATH}") - extract_path("${CLDR_PATH}" "${CLDR_ZIP_PATH}" "${CLDR_UNITS_SOURCE}/**" "${CLDR_UNITS_PATH}") else() message(STATUS "Skipping download of ${CLDR_ZIP_URL}, expecting the archive to have been extracted to ${CLDR_PATH}") endif() @@ -79,7 +75,7 @@ if (ENABLE_UNICODE_DATABASE_DOWNLOAD) "${CLDR_VERSION_FILE}" "${NUMBER_FORMAT_DATA_HEADER}" "${NUMBER_FORMAT_DATA_IMPLEMENTATION}" - arguments -r "${CLDR_CORE_PATH}" -n "${CLDR_NUMBERS_PATH}" -u "${CLDR_UNITS_PATH}" + arguments -r "${CLDR_CORE_PATH}" -n "${CLDR_NUMBERS_PATH}" ) invoke_generator( "PluralRulesData" diff --git a/Meta/Lagom/Tools/CodeGenerators/LibLocale/GenerateNumberFormatData.cpp b/Meta/Lagom/Tools/CodeGenerators/LibLocale/GenerateNumberFormatData.cpp index ff9d6360266..0a5b79c603f 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibLocale/GenerateNumberFormatData.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibLocale/GenerateNumberFormatData.cpp @@ -5,11 +5,8 @@ */ #include "../LibUnicode/GeneratorUtil.h" // FIXME: Move this somewhere common. -#include #include #include -#include -#include #include #include #include @@ -25,144 +22,23 @@ #include #include #include -#include -#include #include -#include #include -enum class NumberFormatType { - Standard, - Compact, -}; - -struct NumberFormat : public Locale::NumberFormat { - using Base = Locale::NumberFormat; - - unsigned hash() const - { - auto hash = pair_int_hash(magnitude, exponent); - hash = pair_int_hash(hash, to_underlying(plurality)); - hash = pair_int_hash(hash, zero_format_index); - hash = pair_int_hash(hash, positive_format_index); - hash = pair_int_hash(hash, negative_format_index); - - for (auto index : identifier_indices) - hash = pair_int_hash(hash, index); - - return hash; - } - - bool operator==(NumberFormat const& other) const - { - return (magnitude == other.magnitude) - && (exponent == other.exponent) - && (plurality == other.plurality) - && (zero_format_index == other.zero_format_index) - && (positive_format_index == other.positive_format_index) - && (negative_format_index == other.negative_format_index) - && (identifier_indices == other.identifier_indices); - } - - size_t zero_format_index { 0 }; - size_t positive_format_index { 0 }; - size_t negative_format_index { 0 }; - Vector identifier_indices {}; -}; - -template<> -struct AK::Formatter : Formatter { - ErrorOr format(FormatBuilder& builder, NumberFormat const& format) - { - StringBuilder identifier_indices; - identifier_indices.join(", "sv, format.identifier_indices); - - return Formatter::format(builder, - "{{ {}, {}, {}, {}, {}, {}, {{ {} }} }}"sv, - format.magnitude, - format.exponent, - to_underlying(format.plurality), - format.zero_format_index, - format.positive_format_index, - format.negative_format_index, - identifier_indices.to_byte_string()); - } -}; - -template<> -struct AK::Traits : public DefaultTraits { - static unsigned hash(NumberFormat const& f) { return f.hash(); } -}; - -using NumberFormatList = Vector; using NumericSymbolList = Vector; struct NumberSystem { - unsigned hash() const - { - auto hash = int_hash(symbols); - hash = pair_int_hash(hash, primary_grouping_size); - hash = pair_int_hash(hash, secondary_grouping_size); - hash = pair_int_hash(hash, decimal_format); - hash = pair_int_hash(hash, decimal_long_formats); - hash = pair_int_hash(hash, decimal_short_formats); - hash = pair_int_hash(hash, currency_format); - hash = pair_int_hash(hash, accounting_format); - hash = pair_int_hash(hash, currency_unit_formats); - hash = pair_int_hash(hash, percent_format); - hash = pair_int_hash(hash, scientific_format); - return hash; - } - - bool operator==(NumberSystem const& other) const - { - return (symbols == other.symbols) - && (primary_grouping_size == other.primary_grouping_size) - && (secondary_grouping_size == other.secondary_grouping_size) - && (decimal_format == other.decimal_format) - && (decimal_long_formats == other.decimal_long_formats) - && (decimal_short_formats == other.decimal_short_formats) - && (currency_format == other.currency_format) - && (accounting_format == other.accounting_format) - && (currency_unit_formats == other.currency_unit_formats) - && (percent_format == other.percent_format) - && (scientific_format == other.scientific_format); - } + unsigned hash() const { return int_hash(symbols); } + bool operator==(NumberSystem const& other) const { return (symbols == other.symbols); } size_t symbols { 0 }; - - u8 primary_grouping_size { 0 }; - u8 secondary_grouping_size { 0 }; - - size_t decimal_format { 0 }; - size_t decimal_long_formats { 0 }; - size_t decimal_short_formats { 0 }; - - size_t currency_format { 0 }; - size_t accounting_format { 0 }; - size_t currency_unit_formats { 0 }; - - size_t percent_format { 0 }; - size_t scientific_format { 0 }; }; template<> struct AK::Formatter : Formatter { ErrorOr format(FormatBuilder& builder, NumberSystem const& system) { - return Formatter::format(builder, - "{{ {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} }}"sv, - system.symbols, - system.primary_grouping_size, - system.secondary_grouping_size, - system.decimal_format, - system.decimal_long_formats, - system.decimal_short_formats, - system.currency_format, - system.accounting_format, - system.currency_unit_formats, - system.percent_format, - system.scientific_format); + return Formatter::format(builder, "{{ {} }}"sv, system.symbols); } }; @@ -171,67 +47,19 @@ struct AK::Traits : public DefaultTraits { static unsigned hash(NumberSystem const& s) { return s.hash(); } }; -struct Unit { - unsigned hash() const - { - auto hash = int_hash(unit); - hash = pair_int_hash(hash, long_formats); - hash = pair_int_hash(hash, short_formats); - hash = pair_int_hash(hash, narrow_formats); - return hash; - } - - bool operator==(Unit const& other) const - { - return (unit == other.unit) - && (long_formats == other.long_formats) - && (short_formats == other.short_formats) - && (narrow_formats == other.narrow_formats); - } - - size_t unit { 0 }; - size_t long_formats { 0 }; - size_t short_formats { 0 }; - size_t narrow_formats { 0 }; -}; - -template<> -struct AK::Formatter : Formatter { - ErrorOr format(FormatBuilder& builder, Unit const& system) - { - return Formatter::format(builder, - "{{ {}, {}, {}, {} }}"sv, - system.unit, - system.long_formats, - system.short_formats, - system.narrow_formats); - } -}; - -template<> -struct AK::Traits : public DefaultTraits { - static unsigned hash(Unit const& u) { return u.hash(); } -}; - struct LocaleData { Vector number_systems; - HashMap units {}; - u8 minimum_grouping_digits { 0 }; }; struct CLDR { UniqueStringStorage unique_strings; - UniqueStorage unique_formats; - UniqueStorage unique_format_lists; UniqueStorage unique_symbols; UniqueStorage unique_systems; - UniqueStorage unique_units; HashMap> number_system_digits; Vector number_systems; HashMap locales; - size_t max_identifier_count { 0 }; }; static ErrorOr parse_number_system_digits(ByteString core_supplemental_path, CLDR& cldr) @@ -266,144 +94,6 @@ static ErrorOr parse_number_system_digits(ByteString core_supplemental_pat return {}; } -static ByteString parse_identifiers(ByteString pattern, StringView replacement, CLDR& cldr, NumberFormat& format) -{ - static constexpr Utf8View whitespace { "\u0020\u00a0\u200f"sv }; - - while (true) { - Utf8View utf8_pattern { pattern }; - Optional start_index; - Optional end_index; - bool inside_replacement = false; - - for (auto it = utf8_pattern.begin(); it != utf8_pattern.end(); ++it) { - if (*it == '{') { - if (start_index.has_value()) { - end_index = utf8_pattern.byte_offset_of(it); - break; - } - - inside_replacement = true; - } else if (*it == '}') { - inside_replacement = false; - } else if (!inside_replacement && !start_index.has_value() && !whitespace.contains(*it)) { - start_index = utf8_pattern.byte_offset_of(it); - } - } - - if (!start_index.has_value()) - return pattern; - - end_index = end_index.value_or(pattern.length()); - - utf8_pattern = utf8_pattern.substring_view(*start_index, *end_index - *start_index); - utf8_pattern = utf8_pattern.trim(whitespace); - - auto identifier = utf8_pattern.as_string().replace("'.'"sv, "."sv, ReplaceMode::FirstOnly); - auto identifier_index = cldr.unique_strings.ensure(move(identifier)); - size_t replacement_index = 0; - - if (auto index = format.identifier_indices.find_first_index(identifier_index); index.has_value()) { - replacement_index = *index; - } else { - replacement_index = format.identifier_indices.size(); - format.identifier_indices.append(identifier_index); - - cldr.max_identifier_count = max(cldr.max_identifier_count, format.identifier_indices.size()); - } - - pattern = ByteString::formatted("{}{{{}:{}}}{}", - *start_index > 0 ? pattern.substring_view(0, *start_index) : ""sv, - replacement, - replacement_index, - pattern.substring_view(*start_index + utf8_pattern.byte_length())); - } -} - -static void parse_number_pattern(Vector patterns, CLDR& cldr, NumberFormatType type, NumberFormat& format, NumberSystem* number_system_for_groupings = nullptr) -{ - // https://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns - // https://cldr.unicode.org/translation/number-currency-formats/number-and-currency-patterns - VERIFY((patterns.size() == 1) || (patterns.size() == 2)); - - auto replace_patterns = [&](ByteString pattern) { - static HashMap replacements = { - { "{0}"sv, "{number}"sv }, - { "{1}"sv, "{currency}"sv }, - { "%"sv, "{percentSign}"sv }, - { "+"sv, "{plusSign}"sv }, - { "-"sv, "{minusSign}"sv }, - { "¤"sv, "{currency}"sv }, // U+00A4 Currency Sign - { "E"sv, "{scientificSeparator}"sv }, - }; - - for (auto const& replacement : replacements) - pattern = pattern.replace(replacement.key, replacement.value, ReplaceMode::All); - - if (auto start_number_index = pattern.find_any_of("#0"sv, ByteString::SearchDirection::Forward); start_number_index.has_value()) { - auto end_number_index = *start_number_index + 1; - - for (; end_number_index < pattern.length(); ++end_number_index) { - auto ch = pattern[end_number_index]; - if ((ch != '#') && (ch != '0') && (ch != ',') && (ch != '.')) - break; - } - - if (number_system_for_groupings) { - auto number_pattern = pattern.substring_view(*start_number_index, end_number_index - *start_number_index); - - auto group_separators = number_pattern.find_all(","sv); - VERIFY((group_separators.size() == 1) || (group_separators.size() == 2)); - - auto decimal = number_pattern.find('.'); - VERIFY(decimal.has_value()); - - if (group_separators.size() == 1) { - number_system_for_groupings->primary_grouping_size = *decimal - group_separators[0] - 1; - number_system_for_groupings->secondary_grouping_size = number_system_for_groupings->primary_grouping_size; - } else { - number_system_for_groupings->primary_grouping_size = *decimal - group_separators[1] - 1; - number_system_for_groupings->secondary_grouping_size = group_separators[1] - group_separators[0] - 1; - } - } - - pattern = ByteString::formatted("{}{{number}}{}", - *start_number_index > 0 ? pattern.substring_view(0, *start_number_index) : ""sv, - pattern.substring_view(end_number_index)); - - // This is specifically handled here rather than in the replacements HashMap above so - // that we do not errantly replace zeroes in number patterns. - if (pattern.contains(*replacements.get("E"sv))) - pattern = pattern.replace("0"sv, "{scientificExponent}"sv, ReplaceMode::FirstOnly); - } - - if (type == NumberFormatType::Compact) - return parse_identifiers(move(pattern), "compactIdentifier"sv, cldr, format); - - return pattern; - }; - - auto zero_format = replace_patterns(move(patterns[0])); - format.positive_format_index = cldr.unique_strings.ensure(ByteString::formatted("{{plusSign}}{}", zero_format)); - - if (patterns.size() == 2) { - auto negative_format = replace_patterns(move(patterns[1])); - format.negative_format_index = cldr.unique_strings.ensure(move(negative_format)); - } else { - format.negative_format_index = cldr.unique_strings.ensure(ByteString::formatted("{{minusSign}}{}", zero_format)); - } - - format.zero_format_index = cldr.unique_strings.ensure(move(zero_format)); -} - -static void parse_number_pattern(Vector patterns, CLDR& cldr, NumberFormatType type, size_t& format_index, NumberSystem* number_system_for_groupings = nullptr) -{ - NumberFormat format {}; - parse_number_pattern(move(patterns), cldr, type, format, number_system_for_groupings); - - format_index = cldr.unique_formats.ensure(move(format)); -} - static ErrorOr parse_number_systems(ByteString locale_numbers_path, CLDR& cldr, LocaleData& locale) { LexicalPath numbers_path(move(locale_numbers_path)); @@ -429,42 +119,6 @@ static ErrorOr parse_number_systems(ByteString locale_numbers_path, CLDR& return number_system.value(); }; - auto parse_number_format = [&](auto const& format_object) { - Vector result; - result.ensure_capacity(format_object.size()); - - format_object.for_each_member([&](auto const& key, JsonValue const& value) { - auto split_key = key.split_view('-'); - if (split_key.size() != 3) - return; - - auto patterns = value.as_string().split(';'); - NumberFormat format {}; - - if (auto type = split_key[0].template to_number(); type.has_value()) { - VERIFY(*type % 10 == 0); - format.magnitude = static_cast(log10(*type)); - - if (patterns[0] != "0"sv) { - auto number_of_zeroes_in_pattern = patterns[0].count("0"sv); - VERIFY(format.magnitude >= number_of_zeroes_in_pattern); - - format.exponent = format.magnitude + 1 - number_of_zeroes_in_pattern; - } - } else { - VERIFY(split_key[0] == "unitPattern"sv); - } - - format.plurality = Locale::plural_category_from_string(split_key[2]); - parse_number_pattern(move(patterns), cldr, NumberFormatType::Compact, format); - - auto format_index = cldr.unique_formats.ensure(move(format)); - result.append(format_index); - }); - - return cldr.unique_format_lists.ensure(move(result)); - }; - auto numeric_symbol_from_string = [&](StringView numeric_symbol) -> Optional { if (numeric_symbol == "approximatelySign"sv) return Locale::NumericSymbol::ApproximatelySign; @@ -491,10 +145,6 @@ static ErrorOr parse_number_systems(ByteString locale_numbers_path, CLDR& locale_numbers_object.for_each_member([&](auto const& key, JsonValue const& value) { constexpr auto symbols_prefix = "symbols-numberSystem-"sv; - constexpr auto decimal_formats_prefix = "decimalFormats-numberSystem-"sv; - constexpr auto currency_formats_prefix = "currencyFormats-numberSystem-"sv; - constexpr auto percent_formats_prefix = "percentFormats-numberSystem-"sv; - constexpr auto scientific_formats_prefix = "scientificFormats-numberSystem-"sv; constexpr auto misc_patterns_prefix = "miscPatterns-numberSystem-"sv; if (key.starts_with(symbols_prefix)) { @@ -532,41 +182,6 @@ static ErrorOr parse_number_systems(ByteString locale_numbers_path, CLDR& symbols[to_underlying(Locale::NumericSymbol::RangeSeparator)] = symbol_index; number_system.symbols = cldr.unique_symbols.ensure(move(symbols)); - } else if (key.starts_with(decimal_formats_prefix)) { - auto system = key.substring(decimal_formats_prefix.length()); - auto& number_system = ensure_number_system(system); - - auto format_object = value.as_object().get_byte_string("standard"sv).value(); - parse_number_pattern(format_object.split(';'), cldr, NumberFormatType::Standard, number_system.decimal_format, &number_system); - - auto const& long_format = value.as_object().get_object("long"sv)->get_object("decimalFormat"sv).value(); - number_system.decimal_long_formats = parse_number_format(long_format); - - auto const& short_format = value.as_object().get_object("short"sv)->get_object("decimalFormat"sv).value(); - number_system.decimal_short_formats = parse_number_format(short_format); - } else if (key.starts_with(currency_formats_prefix)) { - auto system = key.substring(currency_formats_prefix.length()); - auto& number_system = ensure_number_system(system); - - auto format_object = value.as_object().get_byte_string("standard"sv).value(); - parse_number_pattern(format_object.split(';'), cldr, NumberFormatType::Standard, number_system.currency_format); - - format_object = value.as_object().get_byte_string("accounting"sv).value(); - parse_number_pattern(format_object.split(';'), cldr, NumberFormatType::Standard, number_system.accounting_format); - - number_system.currency_unit_formats = parse_number_format(value.as_object()); - } else if (key.starts_with(percent_formats_prefix)) { - auto system = key.substring(percent_formats_prefix.length()); - auto& number_system = ensure_number_system(system); - - auto format_object = value.as_object().get_byte_string("standard"sv).value(); - parse_number_pattern(format_object.split(';'), cldr, NumberFormatType::Standard, number_system.percent_format); - } else if (key.starts_with(scientific_formats_prefix)) { - auto system = key.substring(scientific_formats_prefix.length()); - auto& number_system = ensure_number_system(system); - - auto format_object = value.as_object().get_byte_string("standard"sv).value(); - parse_number_pattern(format_object.split(';'), cldr, NumberFormatType::Standard, number_system.scientific_format); } }); @@ -580,114 +195,11 @@ static ErrorOr parse_number_systems(ByteString locale_numbers_path, CLDR& locale.number_systems.append(system_index); } - locale.minimum_grouping_digits = minimum_grouping_digits.template to_number().value(); + // locale.minimum_grouping_digits = minimum_grouping_digits.template to_number().value(); return {}; } -static ErrorOr parse_units(ByteString locale_units_path, CLDR& cldr, LocaleData& locale) -{ - LexicalPath units_path(move(locale_units_path)); - units_path = units_path.append("units.json"sv); - - auto locale_units = TRY(read_json_file(units_path.string())); - auto const& main_object = locale_units.as_object().get_object("main"sv).value(); - auto const& locale_object = main_object.get_object(units_path.parent().basename()).value(); - auto const& locale_units_object = locale_object.get_object("units"sv).value(); - auto const& long_object = locale_units_object.get_object("long"sv).value(); - auto const& short_object = locale_units_object.get_object("short"sv).value(); - auto const& narrow_object = locale_units_object.get_object("narrow"sv).value(); - - HashMap units; - - auto ensure_unit = [&](auto const& unit) -> Unit& { - return units.ensure(unit, [&]() { - auto unit_index = cldr.unique_strings.ensure(unit); - return Unit { .unit = unit_index }; - }); - }; - - auto is_sanctioned_unit = [](StringView unit_name) { - // LibUnicode generally tries to avoid being directly dependent on ECMA-402, but this rather significantly reduces the amount - // of data generated here, and ECMA-402 is currently the only consumer of this data. - constexpr auto sanctioned_units = JS::Intl::sanctioned_single_unit_identifiers(); - return find(sanctioned_units.begin(), sanctioned_units.end(), unit_name) != sanctioned_units.end(); - }; - - auto parse_units_object = [&](auto const& units_object, Locale::Style style) { - constexpr auto unit_pattern_prefix = "unitPattern-count-"sv; - constexpr auto combined_unit_separator = "-per-"sv; - - units_object.for_each_member([&](auto const& key, JsonValue const& value) { - auto end_of_category = key.find('-'); - if (!end_of_category.has_value()) - return; - - auto unit_name = key.substring(*end_of_category + 1); - - if (!is_sanctioned_unit(unit_name)) { - auto indices = unit_name.find_all(combined_unit_separator); - if (indices.size() != 1) - return; - - auto numerator = unit_name.substring_view(0, indices[0]); - auto denominator = unit_name.substring_view(indices[0] + combined_unit_separator.length()); - if (!is_sanctioned_unit(numerator) || !is_sanctioned_unit(denominator)) - return; - } - - auto& unit = ensure_unit(unit_name); - NumberFormatList formats; - - value.as_object().for_each_member([&](auto const& unit_key, JsonValue const& pattern_value) { - if (!unit_key.starts_with(unit_pattern_prefix)) - return; - - NumberFormat format {}; - - auto plurality = unit_key.substring_view(unit_pattern_prefix.length()); - format.plurality = Locale::plural_category_from_string(plurality); - - auto zero_format = pattern_value.as_string().replace("{0}"sv, "{number}"sv, ReplaceMode::FirstOnly); - zero_format = parse_identifiers(zero_format, "unitIdentifier"sv, cldr, format); - - format.positive_format_index = cldr.unique_strings.ensure(zero_format.replace("{number}"sv, "{plusSign}{number}"sv, ReplaceMode::FirstOnly)); - format.negative_format_index = cldr.unique_strings.ensure(zero_format.replace("{number}"sv, "{minusSign}{number}"sv, ReplaceMode::FirstOnly)); - format.zero_format_index = cldr.unique_strings.ensure(move(zero_format)); - - formats.append(cldr.unique_formats.ensure(move(format))); - }); - - auto number_format_list_index = cldr.unique_format_lists.ensure(move(formats)); - - switch (style) { - case Locale::Style::Long: - unit.long_formats = number_format_list_index; - break; - case Locale::Style::Short: - unit.short_formats = number_format_list_index; - break; - case Locale::Style::Narrow: - unit.narrow_formats = number_format_list_index; - break; - default: - VERIFY_NOT_REACHED(); - } - }); - }; - - parse_units_object(long_object, Locale::Style::Long); - parse_units_object(short_object, Locale::Style::Short); - parse_units_object(narrow_object, Locale::Style::Narrow); - - for (auto& unit : units) { - auto unit_index = cldr.unique_units.ensure(move(unit.value)); - locale.units.set(unit.key, unit_index); - } - - return {}; -} - -static ErrorOr parse_all_locales(ByteString core_path, ByteString numbers_path, ByteString units_path, CLDR& cldr) +static ErrorOr parse_all_locales(ByteString core_path, ByteString numbers_path, CLDR& cldr) { LexicalPath core_supplemental_path(move(core_path)); core_supplemental_path = core_supplemental_path.append("supplemental"sv); @@ -717,19 +229,10 @@ static ErrorOr parse_all_locales(ByteString core_path, ByteString numbers_ return IterationDecision::Continue; })); - TRY(Core::Directory::for_each_entry(TRY(String::formatted("{}/main", units_path)), Core::DirIterator::SkipParentAndBaseDir, [&](auto& entry, auto& directory) -> ErrorOr { - auto units_path = LexicalPath::join(directory.path().string(), entry.name).string(); - auto language = TRY(remove_variants_from_path(units_path)); - - auto& locale = cldr.locales.ensure(language); - TRY(parse_units(units_path, cldr, locale)); - return IterationDecision::Continue; - })); - return {}; } -static ByteString format_identifier(StringView, ByteString identifier) +static ByteString format_identifier(StringView, ByteString const& identifier) { return identifier.to_titlecase(); } @@ -762,23 +265,17 @@ static ErrorOr generate_unicode_locale_implementation(Core::InputBufferedF StringBuilder builder; SourceGenerator generator { builder }; generator.set("string_index_type"sv, cldr.unique_strings.type_that_fits()); - generator.set("number_format_index_type"sv, cldr.unique_formats.type_that_fits()); - generator.set("number_format_list_index_type"sv, cldr.unique_format_lists.type_that_fits()); generator.set("numeric_symbol_list_index_type"sv, cldr.unique_symbols.type_that_fits()); - generator.set("identifier_count", ByteString::number(cldr.max_identifier_count)); generator.append(R"~~~( #include -#include #include #include #include -#include #include #include #include #include -#include namespace Locale { )~~~"); @@ -786,82 +283,19 @@ namespace Locale { cldr.unique_strings.generate(generator); generator.append(R"~~~( -struct NumberFormatImpl { - NumberFormat to_unicode_number_format() const { - NumberFormat number_format {}; - - number_format.magnitude = magnitude; - number_format.exponent = exponent; - number_format.plurality = static_cast(plurality); - number_format.zero_format = decode_string(zero_format); - number_format.positive_format = decode_string(positive_format); - number_format.negative_format = decode_string(negative_format); - - number_format.identifiers.ensure_capacity(identifiers.size()); - for (@string_index_type@ identifier : identifiers) - number_format.identifiers.unchecked_append(decode_string(identifier)); - - return number_format; - } - - u8 magnitude { 0 }; - u8 exponent { 0 }; - u8 plurality { 0 }; - @string_index_type@ zero_format { 0 }; - @string_index_type@ positive_format { 0 }; - @string_index_type@ negative_format { 0 }; - Array<@string_index_type@, @identifier_count@> identifiers {}; -}; - struct NumberSystemData { @numeric_symbol_list_index_type@ symbols { 0 }; - - u8 primary_grouping_size { 0 }; - u8 secondary_grouping_size { 0 }; - - @number_format_index_type@ decimal_format { 0 }; - @number_format_list_index_type@ decimal_long_formats { 0 }; - @number_format_list_index_type@ decimal_short_formats { 0 }; - - @number_format_index_type@ currency_format { 0 }; - @number_format_index_type@ accounting_format { 0 }; - @number_format_list_index_type@ currency_unit_formats { 0 }; - - @number_format_index_type@ percent_format { 0 }; - @number_format_index_type@ scientific_format { 0 }; -}; - -struct Unit { - @string_index_type@ unit { 0 }; - @number_format_list_index_type@ long_formats { 0 }; - @number_format_list_index_type@ short_formats { 0 }; - @number_format_list_index_type@ narrow_formats { 0 }; }; )~~~"); - cldr.unique_formats.generate(generator, "NumberFormatImpl"sv, "s_number_formats"sv, 10); - cldr.unique_format_lists.generate(generator, cldr.unique_formats.type_that_fits(), "s_number_format_lists"sv); cldr.unique_symbols.generate(generator, cldr.unique_strings.type_that_fits(), "s_numeric_symbol_lists"sv); cldr.unique_systems.generate(generator, "NumberSystemData"sv, "s_number_systems"sv, 10); - cldr.unique_units.generate(generator, "Unit"sv, "s_units"sv, 10); auto locales = cldr.locales.keys(); quick_sort(locales); - generator.set("size", ByteString::number(locales.size())); - generator.append(R"~~~( -static constexpr Array s_minimum_grouping_digits { { )~~~"); - - bool first = true; - for (auto const& locale : locales) { - generator.append(first ? " "sv : ", "sv); - generator.append(ByteString::number(cldr.locales.find(locale)->value.minimum_grouping_digits)); - first = false; - } - generator.append(" } };\n"); - auto append_map = [&](ByteString name, auto type, auto const& map) { - generator.set("name", name); + generator.set("name", move(name)); generator.set("type", type); generator.set("size", ByteString::number(map.size())); @@ -883,7 +317,6 @@ static constexpr Array<@type@, @size@> @name@ { {)~~~"); generate_mapping(generator, cldr.number_system_digits, "u32"sv, "s_number_systems_digits"sv, "s_number_systems_digits_{}"sv, nullptr, [&](auto const& name, auto const& value) { append_map(name, "u32"sv, value); }); generate_mapping(generator, cldr.locales, cldr.unique_systems.type_that_fits(), "s_locale_number_systems"sv, "s_number_systems_{}"sv, nullptr, [&](auto const& name, auto const& value) { append_map(name, cldr.unique_systems.type_that_fits(), value.number_systems); }); - generate_mapping(generator, cldr.locales, cldr.unique_units.type_that_fits(), "s_locale_units"sv, "s_units_{}"sv, nullptr, [&](auto const& name, auto const& value) { append_map(name, cldr.unique_units.type_that_fits(), value.units); }); generator.append(R"~~~( static Optional keyword_to_number_system(KeywordNumbers keyword) @@ -969,127 +402,6 @@ Optional get_number_system_symbol(StringView locale, StringView syst return {}; } -Optional get_number_system_groupings(StringView locale, StringView system) -{ - auto locale_value = locale_from_string(locale); - if (!locale_value.has_value()) - return {}; - - u8 minimum_grouping_digits = s_minimum_grouping_digits[to_underlying(*locale_value) - 1]; - - if (auto const* number_system = find_number_system(locale, system); number_system != nullptr) - return NumberGroupings { minimum_grouping_digits, number_system->primary_grouping_size, number_system->secondary_grouping_size }; - return {}; -} - -Optional get_standard_number_system_format(StringView locale, StringView system, StandardNumberFormatType type) -{ - if (auto const* number_system = find_number_system(locale, system); number_system != nullptr) { - @number_format_index_type@ format_index = 0; - - switch (type) { - case StandardNumberFormatType::Decimal: - format_index = number_system->decimal_format; - break; - case StandardNumberFormatType::Currency: - format_index = number_system->currency_format; - break; - case StandardNumberFormatType::Accounting: - format_index = number_system->accounting_format; - break; - case StandardNumberFormatType::Percent: - format_index = number_system->percent_format; - break; - case StandardNumberFormatType::Scientific: - format_index = number_system->scientific_format; - break; - } - - return s_number_formats[format_index].to_unicode_number_format(); - } - - return {}; -} - -Vector get_compact_number_system_formats(StringView locale, StringView system, CompactNumberFormatType type) -{ - Vector formats; - - if (auto const* number_system = find_number_system(locale, system); number_system != nullptr) { - @number_format_list_index_type@ number_format_list_index { 0 }; - - switch (type) { - case CompactNumberFormatType::DecimalLong: - number_format_list_index = number_system->decimal_long_formats; - break; - case CompactNumberFormatType::DecimalShort: - number_format_list_index = number_system->decimal_short_formats; - break; - case CompactNumberFormatType::CurrencyUnit: - number_format_list_index = number_system->currency_unit_formats; - break; - } - - auto number_formats = s_number_format_lists.at(number_format_list_index); - formats.ensure_capacity(number_formats.size()); - - for (auto number_format : number_formats) - formats.unchecked_append(s_number_formats[number_format].to_unicode_number_format()); - } - - return formats; -} - -static Unit const* find_units(StringView locale, StringView unit) -{ - auto locale_value = locale_from_string(locale); - if (!locale_value.has_value()) - return nullptr; - - auto locale_index = to_underlying(*locale_value) - 1; // Subtract 1 because 0 == Locale::None. - auto const& locale_units = s_locale_units.at(locale_index); - - for (auto unit_index : locale_units) { - auto const& units = s_units.at(unit_index); - - if (unit == decode_string(units.unit)) - return &units; - }; - - return nullptr; -} - -Vector get_unit_formats(StringView locale, StringView unit, Style style) -{ - Vector formats; - - if (auto const* units = find_units(locale, unit); units != nullptr) { - @number_format_list_index_type@ number_format_list_index { 0 }; - - switch (style) { - case Style::Long: - number_format_list_index = units->long_formats; - break; - case Style::Short: - number_format_list_index = units->short_formats; - break; - case Style::Narrow: - number_format_list_index = units->narrow_formats; - break; - default: - VERIFY_NOT_REACHED(); - } - - auto number_formats = s_number_format_lists.at(number_format_list_index); - formats.ensure_capacity(number_formats.size()); - - for (auto number_format : number_formats) - formats.unchecked_append(s_number_formats[number_format].to_unicode_number_format()); - } - - return formats; -} - } )~~~"); @@ -1103,21 +415,19 @@ ErrorOr serenity_main(Main::Arguments arguments) StringView generated_implementation_path; StringView core_path; StringView numbers_path; - StringView units_path; Core::ArgsParser args_parser; args_parser.add_option(generated_header_path, "Path to the Unicode locale header file to generate", "generated-header-path", 'h', "generated-header-path"); args_parser.add_option(generated_implementation_path, "Path to the Unicode locale implementation file to generate", "generated-implementation-path", 'c', "generated-implementation-path"); args_parser.add_option(core_path, "Path to cldr-core directory", "core-path", 'r', "core-path"); args_parser.add_option(numbers_path, "Path to cldr-numbers directory", "numbers-path", 'n', "numbers-path"); - args_parser.add_option(units_path, "Path to cldr-units directory", "units-path", 'u', "units-path"); args_parser.parse(arguments); auto generated_header_file = TRY(open_file(generated_header_path, Core::File::OpenMode::Write)); auto generated_implementation_file = TRY(open_file(generated_implementation_path, Core::File::OpenMode::Write)); CLDR cldr; - TRY(parse_all_locales(core_path, numbers_path, units_path, cldr)); + TRY(parse_all_locales(core_path, numbers_path, cldr)); TRY(generate_unicode_locale_header(*generated_header_file, cldr)); TRY(generate_unicode_locale_implementation(*generated_implementation_file, cldr)); diff --git a/Userland/Libraries/LibJS/Runtime/BigIntPrototype.cpp b/Userland/Libraries/LibJS/Runtime/BigIntPrototype.cpp index 32ff899648a..6047a0d6fa4 100644 --- a/Userland/Libraries/LibJS/Runtime/BigIntPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/BigIntPrototype.cpp @@ -93,7 +93,7 @@ JS_DEFINE_NATIVE_FUNCTION(BigIntPrototype::to_locale_string) auto* number_format = static_cast(TRY(construct(vm, realm.intrinsics().intl_number_format_constructor(), locales, options)).ptr()); // 3. Return ? FormatNumeric(numberFormat, x). - auto formatted = Intl::format_numeric(vm, *number_format, Value(bigint)); + auto formatted = Intl::format_numeric(*number_format, Value(bigint)); return PrimitiveString::create(vm, move(formatted)); } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h index ea1593adf68..5c0a686c6b0 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/AbstractOperations.h @@ -56,7 +56,8 @@ struct PatternPartition { }; struct PatternPartitionWithSource : public PatternPartition { - static Vector create_from_parent_list(Vector partitions) + template + static Vector create_from_parent_list(ParentList partitions) { Vector result; result.ensure_capacity(partitions.size()); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp index d163caa96d1..a85ba1227c1 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp @@ -541,7 +541,7 @@ ThrowCompletionOr> format_date_time_pattern(VM& vm, Dat value = floor(value * pow(10, static_cast(*fractional_second_digits) - 3)); // iii. Let fv be FormatNumeric(nf3, v). - auto formatted_value = format_numeric(vm, *number_format3, Value(value)); + auto formatted_value = format_numeric(*number_format3, Value(value)); // iv. Append a new Record { [[Type]]: "fractionalSecond", [[Value]]: fv } as the last element of result. result.append({ "fractionalSecond"sv, move(formatted_value) }); @@ -625,13 +625,13 @@ ThrowCompletionOr> format_date_time_pattern(VM& vm, Dat // viii. If f is "numeric", then case ::Locale::CalendarPatternStyle::Numeric: // 1. Let fv be FormatNumeric(nf, v). - formatted_value = format_numeric(vm, *number_format, Value(value)); + formatted_value = format_numeric(*number_format, Value(value)); break; // ix. Else if f is "2-digit", then case ::Locale::CalendarPatternStyle::TwoDigit: // 1. Let fv be FormatNumeric(nf2, v). - formatted_value = format_numeric(vm, *number_format2, Value(value)); + formatted_value = format_numeric(*number_format2, Value(value)); // 2. If the "length" property of fv is greater than 2, let fv be the substring of fv containing the last two characters. // NOTE: The first length check here isn't enough, but lets us avoid UTF-16 transcoding when the formatted value is ASCII. diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp index 073f1719f49..4576ccf2ac2 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/DurationFormat.cpp @@ -449,7 +449,7 @@ Vector<::Locale::ListFormatPart> partition_duration_format_pattern(VM& vm, Durat // 3. Let dataLocaleData be %DurationFormat%.[[LocaleData]].[[]]. // 4. Let num be ! FormatNumeric(nf, 𝔽(value)). - auto number = format_numeric(vm, *number_format, MathematicalValue(value)); + auto number = format_numeric(*number_format, MathematicalValue(value)); // 5. Append the new Record { [[Type]]: unit, [[Value]]: num} to the end of result. result.append({ unit, move(number) }); @@ -508,7 +508,7 @@ Vector<::Locale::ListFormatPart> partition_duration_format_pattern(VM& vm, Durat auto* number_format = static_cast(MUST(construct(vm, realm.intrinsics().intl_number_format_constructor(), PrimitiveString::create(vm, duration_format.locale()), number_format_options)).ptr()); // 5. Let parts be ! PartitionNumberPattern(nf, 𝔽(value)). - auto parts = partition_number_pattern(vm, *number_format, MathematicalValue(value)); + auto parts = partition_number_pattern(*number_format, MathematicalValue(value)); // 6. Let concat be an empty String. StringBuilder concat; diff --git a/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.cpp b/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.cpp index 8b1e9c3b165..d3c89e03417 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.cpp @@ -1,13 +1,10 @@ /* - * Copyright (c) 2022, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ -#include -#include #include -#include namespace JS::Intl { @@ -66,265 +63,25 @@ bool MathematicalValue::is_nan() const return m_value.get() == Symbol::NotANumber; } -void MathematicalValue::negate() -{ - m_value.visit( - [](double& value) { - VERIFY(value != 0.0); - value *= -1.0; - }, - [](Crypto::SignedBigInteger& value) { value.negate(); }, - [](auto) { VERIFY_NOT_REACHED(); }); -} - -MathematicalValue MathematicalValue::plus(Checked addition) const +::Locale::NumberFormat::Value MathematicalValue::to_value() const { return m_value.visit( - [&](double value) { - return MathematicalValue { value + addition.value() }; + [](double value) -> ::Locale::NumberFormat::Value { + return value; }, - [&](Crypto::SignedBigInteger const& value) { - return MathematicalValue { value.plus(Crypto::SignedBigInteger { addition.value() }) }; - }, - [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); -} - -MathematicalValue MathematicalValue::plus(MathematicalValue const& addition) const -{ - return m_value.visit( - [&](double value) { - return MathematicalValue { value + addition.as_number() }; - }, - [&](Crypto::SignedBigInteger const& value) { - return MathematicalValue { value.plus(addition.as_bigint()) }; - }, - [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); -} - -MathematicalValue MathematicalValue::minus(Checked subtraction) const -{ - return m_value.visit( - [&](double value) { - return MathematicalValue { value - subtraction.value() }; - }, - [&](Crypto::SignedBigInteger const& value) { - return MathematicalValue { value.minus(Crypto::SignedBigInteger { subtraction.value() }) }; - }, - [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); -} - -MathematicalValue MathematicalValue::minus(MathematicalValue const& subtraction) const -{ - return m_value.visit( - [&](double value) { - return MathematicalValue { value - subtraction.as_number() }; - }, - [&](Crypto::SignedBigInteger const& value) { - return MathematicalValue { value.minus(subtraction.as_bigint()) }; - }, - [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); -} - -MathematicalValue MathematicalValue::multiplied_by(Checked multiplier) const -{ - return m_value.visit( - [&](double value) { - return MathematicalValue { value * multiplier.value() }; - }, - [&](Crypto::SignedBigInteger const& value) { - return MathematicalValue { value.multiplied_by(Crypto::SignedBigInteger { multiplier.value() }) }; - }, - [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); -} - -MathematicalValue MathematicalValue::multiplied_by(MathematicalValue const& multiplier) const -{ - return m_value.visit( - [&](double value) { - return MathematicalValue { value * multiplier.as_number() }; - }, - [&](Crypto::SignedBigInteger const& value) { - return MathematicalValue { value.multiplied_by(multiplier.as_bigint()) }; - }, - [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); -} - -MathematicalValue MathematicalValue::divided_by(Checked divisor) const -{ - return m_value.visit( - [&](double value) { - return MathematicalValue { value / divisor.value() }; - }, - [&](Crypto::SignedBigInteger const& value) { - return MathematicalValue { value.divided_by(Crypto::SignedBigInteger { divisor.value() }).quotient }; - }, - [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); -} - -MathematicalValue MathematicalValue::divided_by(MathematicalValue const& divisor) const -{ - return m_value.visit( - [&](double value) { - return MathematicalValue { value / divisor.as_number() }; - }, - [&](Crypto::SignedBigInteger const& value) { - return MathematicalValue { value.divided_by(divisor.as_bigint()).quotient }; - }, - [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); -} - -static Crypto::SignedBigInteger bigint_power(Checked exponent) -{ - VERIFY(exponent >= 0); - - static auto base = Crypto::SignedBigInteger { 10 }; - auto result = Crypto::SignedBigInteger { 1 }; - - for (i32 i = 0; i < exponent; ++i) - result = result.multiplied_by(base); - - return result; -} - -MathematicalValue MathematicalValue::multiplied_by_power(Checked exponent) const -{ - return m_value.visit( - [&](double value) { - return MathematicalValue { value * pow(10, exponent.value()) }; - }, - [&](Crypto::SignedBigInteger const& value) { - if (exponent < 0) - return MathematicalValue { value.divided_by(bigint_power(-exponent.value())).quotient }; - return MathematicalValue { value.multiplied_by(bigint_power(exponent)) }; - }, - [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); -} - -MathematicalValue MathematicalValue::divided_by_power(Checked exponent) const -{ - return m_value.visit( - [&](double value) { - if (exponent < 0) - return MathematicalValue { value * pow(10, -exponent.value()) }; - return MathematicalValue { value / pow(10, exponent.value()) }; - }, - [&](Crypto::SignedBigInteger const& value) { - if (exponent < 0) - return MathematicalValue { value.multiplied_by(bigint_power(-exponent.value())) }; - return MathematicalValue { value.divided_by(bigint_power(exponent)).quotient }; - }, - [](auto) -> MathematicalValue { VERIFY_NOT_REACHED(); }); -} - -bool MathematicalValue::modulo_is_zero(Checked mod) const -{ - return m_value.visit( - [&](double value) { - auto result = MathematicalValue { modulo(value, mod.value()) }; - return result.is_equal_to(MathematicalValue { 0.0 }); - }, - [&](Crypto::SignedBigInteger const& value) { - return modulo(value, Crypto::SignedBigInteger { mod.value() }).is_zero(); - }, - [](auto) -> bool { VERIFY_NOT_REACHED(); }); -} - -int MathematicalValue::logarithmic_floor() const -{ - return m_value.visit( - [](double value) { - return static_cast(floor(log10(value))); - }, - [&](Crypto::SignedBigInteger const& value) { - // FIXME: Can we do this without string conversion? - auto value_as_string = MUST(value.to_base(10)); - return static_cast(value_as_string.bytes_as_string_view().length() - 1); - }, - [](auto) -> int { VERIFY_NOT_REACHED(); }); -} - -bool MathematicalValue::is_equal_to(MathematicalValue const& other) const -{ - return m_value.visit( - [&](double value) { - static constexpr double epsilon = 5e-14; - return fabs(value - other.as_number()) < epsilon; - }, - [&](Crypto::SignedBigInteger const& value) { - return value == other.as_bigint(); - }, - [](auto) -> bool { VERIFY_NOT_REACHED(); }); -} - -bool MathematicalValue::is_less_than(MathematicalValue const& other) const -{ - return m_value.visit( - [&](double value) { - if (is_equal_to(other)) - return false; - return value < other.as_number(); - }, - [&](Crypto::SignedBigInteger const& value) { - return value < other.as_bigint(); - }, - [](auto) -> bool { VERIFY_NOT_REACHED(); }); -} - -bool MathematicalValue::is_negative() const -{ - return m_value.visit( - [](double value) { return value < 0.0; }, - [](Crypto::SignedBigInteger const& value) { return value.is_negative(); }, - [](Symbol symbol) { return symbol == Symbol::NegativeInfinity; }); -} - -bool MathematicalValue::is_positive() const -{ - return m_value.visit( - [](double value) { return value > 0.0; }, - [](Crypto::SignedBigInteger const& value) { return !value.is_zero() && !value.is_negative(); }, - [](Symbol symbol) { return symbol == Symbol::PositiveInfinity; }); -} - -bool MathematicalValue::is_zero() const -{ - return m_value.visit( - [&](double value) { return value == 0.0; }, - [](Crypto::SignedBigInteger const& value) { return value.is_zero(); }, - [](auto) { return false; }); -} - -String MathematicalValue::to_string() const -{ - return m_value.visit( - [&](double value) { - return number_to_string(value, NumberToStringMode::WithoutExponent); - }, - [&](Crypto::SignedBigInteger const& value) { + [](Crypto::SignedBigInteger const& value) -> ::Locale::NumberFormat::Value { return MUST(value.to_base(10)); }, - [&](auto) -> String { VERIFY_NOT_REACHED(); }); -} - -Value MathematicalValue::to_value(VM& vm) const -{ - return m_value.visit( - [](double value) { - return Value(value); - }, - [&](Crypto::SignedBigInteger const& value) { - return Value(BigInt::create(vm, value)); - }, - [](auto symbol) { + [](auto symbol) -> ::Locale::NumberFormat::Value { switch (symbol) { case Symbol::PositiveInfinity: - return js_infinity(); + return js_infinity().as_double(); case Symbol::NegativeInfinity: - return js_negative_infinity(); + return js_negative_infinity().as_double(); case Symbol::NegativeZero: - return Value(-0.0); + return -0.0; case Symbol::NotANumber: - return js_nan(); + return js_nan().as_double(); } VERIFY_NOT_REACHED(); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.h b/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.h index 8547c82272d..e3a6c5d3966 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/MathematicalValue.h @@ -1,17 +1,16 @@ /* - * Copyright (c) 2022, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ #pragma once -#include -#include #include #include #include #include +#include namespace JS::Intl { @@ -61,36 +60,7 @@ public: bool is_negative_zero() const; bool is_nan() const; - void negate(); - - MathematicalValue plus(Checked addition) const; - MathematicalValue plus(MathematicalValue const& addition) const; - - MathematicalValue minus(Checked subtraction) const; - MathematicalValue minus(MathematicalValue const& subtraction) const; - - MathematicalValue multiplied_by(Checked multiplier) const; - MathematicalValue multiplied_by(MathematicalValue const& multiplier) const; - - MathematicalValue divided_by(Checked divisor) const; - MathematicalValue divided_by(MathematicalValue const& divisor) const; - - MathematicalValue multiplied_by_power(Checked exponent) const; - MathematicalValue divided_by_power(Checked exponent) const; - - bool modulo_is_zero(Checked mod) const; - - int logarithmic_floor() const; - - bool is_equal_to(MathematicalValue const&) const; - bool is_less_than(MathematicalValue const&) const; - - bool is_negative() const; - bool is_positive() const; - bool is_zero() const; - - String to_string() const; - Value to_value(VM&) const; + ::Locale::NumberFormat::Value to_value() const; private: using ValueType = Variant; diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp index e03c9a984fb..6eae17152c2 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.cpp @@ -44,138 +44,6 @@ void NumberFormat::visit_edges(Cell::Visitor& visitor) visitor.visit(m_bound_format); } -void NumberFormat::set_style(StringView style) -{ - if (style == "decimal"sv) - m_style = Style::Decimal; - else if (style == "percent"sv) - m_style = Style::Percent; - else if (style == "currency"sv) - m_style = Style::Currency; - else if (style == "unit"sv) - m_style = Style::Unit; - else - VERIFY_NOT_REACHED(); -} - -StringView NumberFormat::style_string() const -{ - switch (m_style) { - case Style::Decimal: - return "decimal"sv; - case Style::Percent: - return "percent"sv; - case Style::Currency: - return "currency"sv; - case Style::Unit: - return "unit"sv; - default: - VERIFY_NOT_REACHED(); - } -} - -void NumberFormat::set_currency_display(StringView currency_display) -{ - m_resolved_currency_display.clear(); - - if (currency_display == "code"sv) - m_currency_display = CurrencyDisplay::Code; - else if (currency_display == "symbol"sv) - m_currency_display = CurrencyDisplay::Symbol; - else if (currency_display == "narrowSymbol"sv) - m_currency_display = CurrencyDisplay::NarrowSymbol; - else if (currency_display == "name"sv) - m_currency_display = CurrencyDisplay::Name; - else - VERIFY_NOT_REACHED(); -} - -StringView NumberFormat::resolve_currency_display() -{ - if (m_resolved_currency_display.has_value()) - return *m_resolved_currency_display; - - switch (currency_display()) { - case NumberFormat::CurrencyDisplay::Code: - m_resolved_currency_display = currency(); - break; - case NumberFormat::CurrencyDisplay::Symbol: - m_resolved_currency_display = ::Locale::currency_display_name(data_locale(), currency(), ::Locale::Style::Short); - break; - case NumberFormat::CurrencyDisplay::NarrowSymbol: - m_resolved_currency_display = ::Locale::currency_display_name(data_locale(), currency(), ::Locale::Style::Narrow); - break; - case NumberFormat::CurrencyDisplay::Name: - m_resolved_currency_display = ::Locale::currency_numeric_display_name(data_locale(), currency()); - break; - default: - VERIFY_NOT_REACHED(); - } - - if (!m_resolved_currency_display.has_value()) - m_resolved_currency_display = currency(); - - return *m_resolved_currency_display; -} - -StringView NumberFormat::currency_display_string() const -{ - VERIFY(m_currency_display.has_value()); - - switch (*m_currency_display) { - case CurrencyDisplay::Code: - return "code"sv; - case CurrencyDisplay::Symbol: - return "symbol"sv; - case CurrencyDisplay::NarrowSymbol: - return "narrowSymbol"sv; - case CurrencyDisplay::Name: - return "name"sv; - default: - VERIFY_NOT_REACHED(); - } -} - -void NumberFormat::set_currency_sign(StringView currency_sign) -{ - if (currency_sign == "standard"sv) - m_currency_sign = CurrencySign::Standard; - else if (currency_sign == "accounting"sv) - m_currency_sign = CurrencySign::Accounting; - else - VERIFY_NOT_REACHED(); -} - -StringView NumberFormat::currency_sign_string() const -{ - VERIFY(m_currency_sign.has_value()); - - switch (*m_currency_sign) { - case CurrencySign::Standard: - return "standard"sv; - case CurrencySign::Accounting: - return "accounting"sv; - default: - VERIFY_NOT_REACHED(); - } -} - -StringView NumberFormatBase::rounding_type_string() const -{ - switch (m_rounding_type) { - case RoundingType::SignificantDigits: - return "significantDigits"sv; - case RoundingType::FractionDigits: - return "fractionDigits"sv; - case RoundingType::MorePrecision: - return "morePrecision"sv; - case RoundingType::LessPrecision: - return "lessPrecision"sv; - default: - VERIFY_NOT_REACHED(); - } -} - StringView NumberFormatBase::computed_rounding_priority_string() const { switch (m_computed_rounding_priority) { @@ -190,88 +58,14 @@ StringView NumberFormatBase::computed_rounding_priority_string() const } } -StringView NumberFormatBase::rounding_mode_string() const -{ - switch (m_rounding_mode) { - case RoundingMode::Ceil: - return "ceil"sv; - case RoundingMode::Expand: - return "expand"sv; - case RoundingMode::Floor: - return "floor"sv; - case RoundingMode::HalfCeil: - return "halfCeil"sv; - case RoundingMode::HalfEven: - return "halfEven"sv; - case RoundingMode::HalfExpand: - return "halfExpand"sv; - case RoundingMode::HalfFloor: - return "halfFloor"sv; - case RoundingMode::HalfTrunc: - return "halfTrunc"sv; - case RoundingMode::Trunc: - return "trunc"sv; - default: - VERIFY_NOT_REACHED(); - } -} - -void NumberFormatBase::set_rounding_mode(StringView rounding_mode) -{ - if (rounding_mode == "ceil"sv) - m_rounding_mode = RoundingMode::Ceil; - else if (rounding_mode == "expand"sv) - m_rounding_mode = RoundingMode::Expand; - else if (rounding_mode == "floor"sv) - m_rounding_mode = RoundingMode::Floor; - else if (rounding_mode == "halfCeil"sv) - m_rounding_mode = RoundingMode::HalfCeil; - else if (rounding_mode == "halfEven"sv) - m_rounding_mode = RoundingMode::HalfEven; - else if (rounding_mode == "halfExpand"sv) - m_rounding_mode = RoundingMode::HalfExpand; - else if (rounding_mode == "halfFloor"sv) - m_rounding_mode = RoundingMode::HalfFloor; - else if (rounding_mode == "halfTrunc"sv) - m_rounding_mode = RoundingMode::HalfTrunc; - else if (rounding_mode == "trunc"sv) - m_rounding_mode = RoundingMode::Trunc; - else - VERIFY_NOT_REACHED(); -} - -StringView NumberFormatBase::trailing_zero_display_string() const -{ - switch (m_trailing_zero_display) { - case TrailingZeroDisplay::Auto: - return "auto"sv; - case TrailingZeroDisplay::StripIfInteger: - return "stripIfInteger"sv; - default: - VERIFY_NOT_REACHED(); - } -} - -void NumberFormatBase::set_trailing_zero_display(StringView trailing_zero_display) -{ - if (trailing_zero_display == "auto"sv) - m_trailing_zero_display = TrailingZeroDisplay::Auto; - else if (trailing_zero_display == "stripIfInteger"sv) - m_trailing_zero_display = TrailingZeroDisplay::StripIfInteger; - else - VERIFY_NOT_REACHED(); -} - Value NumberFormat::use_grouping_to_value(VM& vm) const { switch (m_use_grouping) { - case UseGrouping::Always: - return PrimitiveString::create(vm, "always"_string); - case UseGrouping::Auto: - return PrimitiveString::create(vm, "auto"_string); - case UseGrouping::Min2: - return PrimitiveString::create(vm, "min2"_string); - case UseGrouping::False: + case ::Locale::Grouping::Always: + case ::Locale::Grouping::Auto: + case ::Locale::Grouping::Min2: + return PrimitiveString::create(vm, ::Locale::grouping_to_string(m_use_grouping)); + case ::Locale::Grouping::False: return Value(false); default: VERIFY_NOT_REACHED(); @@ -282,107 +76,43 @@ void NumberFormat::set_use_grouping(StringOrBoolean const& use_grouping) { use_grouping.visit( [this](StringView grouping) { - if (grouping == "always"sv) - m_use_grouping = UseGrouping::Always; - else if (grouping == "auto"sv) - m_use_grouping = UseGrouping::Auto; - else if (grouping == "min2"sv) - m_use_grouping = UseGrouping::Min2; - else - VERIFY_NOT_REACHED(); + m_use_grouping = ::Locale::grouping_from_string(grouping); }, [this](bool grouping) { VERIFY(!grouping); - m_use_grouping = UseGrouping::False; + m_use_grouping = ::Locale::Grouping::False; }); } -void NumberFormat::set_notation(StringView notation) +::Locale::RoundingOptions NumberFormatBase::rounding_options() const { - if (notation == "standard"sv) - m_notation = Notation::Standard; - else if (notation == "scientific"sv) - m_notation = Notation::Scientific; - else if (notation == "engineering"sv) - m_notation = Notation::Engineering; - else if (notation == "compact"sv) - m_notation = Notation::Compact; - else - VERIFY_NOT_REACHED(); + return { + .type = m_rounding_type, + .mode = m_rounding_mode, + .trailing_zero_display = m_trailing_zero_display, + .min_significant_digits = m_min_significant_digits, + .max_significant_digits = m_max_significant_digits, + .min_fraction_digits = m_min_fraction_digits, + .max_fraction_digits = m_max_fraction_digits, + .min_integer_digits = m_min_integer_digits, + .rounding_increment = m_rounding_increment + }; } -StringView NumberFormat::notation_string() const +::Locale::DisplayOptions NumberFormat::display_options() const { - switch (m_notation) { - case Notation::Standard: - return "standard"sv; - case Notation::Scientific: - return "scientific"sv; - case Notation::Engineering: - return "engineering"sv; - case Notation::Compact: - return "compact"sv; - default: - VERIFY_NOT_REACHED(); - } -} - -void NumberFormat::set_compact_display(StringView compact_display) -{ - if (compact_display == "short"sv) - m_compact_display = CompactDisplay::Short; - else if (compact_display == "long"sv) - m_compact_display = CompactDisplay::Long; - else - VERIFY_NOT_REACHED(); -} - -StringView NumberFormat::compact_display_string() const -{ - VERIFY(m_compact_display.has_value()); - - switch (*m_compact_display) { - case CompactDisplay::Short: - return "short"sv; - case CompactDisplay::Long: - return "long"sv; - default: - VERIFY_NOT_REACHED(); - } -} - -void NumberFormat::set_sign_display(StringView sign_display) -{ - if (sign_display == "auto"sv) - m_sign_display = SignDisplay::Auto; - else if (sign_display == "never"sv) - m_sign_display = SignDisplay::Never; - else if (sign_display == "always"sv) - m_sign_display = SignDisplay::Always; - else if (sign_display == "exceptZero"sv) - m_sign_display = SignDisplay::ExceptZero; - else if (sign_display == "negative"sv) - m_sign_display = SignDisplay::Negative; - else - VERIFY_NOT_REACHED(); -} - -StringView NumberFormat::sign_display_string() const -{ - switch (m_sign_display) { - case SignDisplay::Auto: - return "auto"sv; - case SignDisplay::Never: - return "never"sv; - case SignDisplay::Always: - return "always"sv; - case SignDisplay::ExceptZero: - return "exceptZero"sv; - case SignDisplay::Negative: - return "negative"sv; - default: - VERIFY_NOT_REACHED(); - } + return { + .style = m_style, + .sign_display = m_sign_display, + .notation = m_notation, + .compact_display = m_compact_display, + .grouping = m_use_grouping, + .currency = m_currency, + .currency_display = m_currency_display, + .currency_sign = m_currency_sign, + .unit = m_unit, + .unit_display = m_unit_display, + }; } // 15.5.1 CurrencyDigits ( currency ), https://tc39.es/ecma402/#sec-currencydigits @@ -396,555 +126,35 @@ int currency_digits(StringView currency) } // 15.5.3 FormatNumericToString ( intlObject, x ), https://tc39.es/ecma402/#sec-formatnumberstring -FormatResult format_numeric_to_string(NumberFormatBase const& intl_object, MathematicalValue number) +String format_numeric_to_string(NumberFormatBase const& intl_object, MathematicalValue const& number) { - bool is_negative = false; - - // 1. If x is negative-zero, then - if (number.is_negative_zero()) { - // a. Let isNegative be true. - is_negative = true; - - // b. Set x to 0. - number = MathematicalValue(0.0); - } - // 2. Else, - else { - // a. Assert: x is a mathematical value. - VERIFY(number.is_mathematical_value()); - - // b. If x < 0, let isNegative be true; else let isNegative be false. - is_negative = number.is_negative(); - - // c. If isNegative is true, then - if (is_negative) { - // i. Set x to -x. - number.negate(); - } - } - - // 3. Let unsignedRoundingMode be GetUnsignedRoundingMode(intlObject.[[RoundingMode]], isNegative). - auto unsigned_rounding_mode = get_unsigned_rounding_mode(intl_object.rounding_mode(), is_negative); - - RawFormatResult result {}; - - switch (intl_object.rounding_type()) { - // 4. If intlObject.[[RoundingType]] is significantDigits, then - case NumberFormatBase::RoundingType::SignificantDigits: - // a. Let result be ToRawPrecision(x, intlObject.[[MinimumSignificantDigits]], intlObject.[[MaximumSignificantDigits]], unsignedRoundingMode). - result = to_raw_precision(number, intl_object.min_significant_digits(), intl_object.max_significant_digits(), unsigned_rounding_mode); - break; - - // 5. Else if intlObject.[[RoundingType]] is fractionDigits, then - case NumberFormatBase::RoundingType::FractionDigits: - // a. Let result be ToRawFixed(x, intlObject.[[MinimumFractionDigits]], intlObject.[[MaximumFractionDigits]], intlObject.[[RoundingIncrement]], unsignedRoundingMode). - result = to_raw_fixed(number, intl_object.min_fraction_digits(), intl_object.max_fraction_digits(), intl_object.rounding_increment(), unsigned_rounding_mode); - break; - - // 6. Else, - case NumberFormatBase::RoundingType::MorePrecision: - case NumberFormatBase::RoundingType::LessPrecision: { - // a. Let sResult be ToRawPrecision(x, intlObject.[[MinimumSignificantDigits]], intlObject.[[MaximumSignificantDigits]], unsignedRoundingMode). - auto significant_result = to_raw_precision(number, intl_object.min_significant_digits(), intl_object.max_significant_digits(), unsigned_rounding_mode); - - // b. Let fResult be ToRawFixed(x, intlObject.[[MinimumFractionDigits]], intlObject.[[MaximumFractionDigits]], intlObject.[[RoundingIncrement]], unsignedRoundingMode). - auto fraction_result = to_raw_fixed(number, intl_object.min_fraction_digits(), intl_object.max_fraction_digits(), intl_object.rounding_increment(), unsigned_rounding_mode); - - // c. If intlObj.[[RoundingType]] is morePrecision, then - if (intl_object.rounding_type() == NumberFormatBase::RoundingType::MorePrecision) { - // i. If sResult.[[RoundingMagnitude]] ≤ fResult.[[RoundingMagnitude]], then - if (significant_result.rounding_magnitude <= fraction_result.rounding_magnitude) { - // 1. Let result be sResult. - result = move(significant_result); - } - // ii. Else, - else { - // 2. Let result be fResult. - result = move(fraction_result); - } - } - // d. Else, - else { - // i. Assert: intlObj.[[RoundingType]] is lessPrecision. - VERIFY(intl_object.rounding_type() == NumberFormatBase::RoundingType::LessPrecision); - - // ii. If sResult.[[RoundingMagnitude]] ≤ fResult.[[RoundingMagnitude]], then - if (significant_result.rounding_magnitude <= fraction_result.rounding_magnitude) { - // 1. Let result be fResult. - result = move(fraction_result); - } - // iii. Else, - else { - // 1. Let result be sResult. - result = move(significant_result); - } - } - - break; - } - - default: - VERIFY_NOT_REACHED(); - } - - // 7. Set x to result.[[RoundedNumber]]. - number = move(result.rounded_number); - - // 8. Let string be result.[[FormattedString]]. - auto string = move(result.formatted_string); - - // 9. If intlObject.[[TrailingZeroDisplay]] is "stripIfInteger" and x modulo 1 = 0, then - if ((intl_object.trailing_zero_display() == NumberFormat::TrailingZeroDisplay::StripIfInteger) && number.modulo_is_zero(1)) { - // a. Let i be StringIndexOf(string, ".", 0). - auto index = string.find_byte_offset('.'); - - // b. If i ≠ -1, set string to the substring of string from 0 to i. - if (index.has_value()) - string = MUST(string.substring_from_byte_offset(0, *index)); - } - - // 10. Let int be result.[[IntegerDigitsCount]]. - int digits = result.digits; - - // 11. Let minInteger be intlObject.[[MinimumIntegerDigits]]. - int min_integer = intl_object.min_integer_digits(); - - // 12. If int < minInteger, then - if (digits < min_integer) { - // a. Let forwardZeros be the String consisting of minInteger - int occurrences of the code unit 0x0030 (DIGIT ZERO). - auto forward_zeros = MUST(String::repeated('0', min_integer - digits)); - - // b. Set string to the string-concatenation of forwardZeros and string. - string = MUST(String::formatted("{}{}", forward_zeros, string)); - } - - // 13. If isNegative is true, then - if (is_negative) { - // a. If x is 0, set x to negative-zero. Otherwise, set x to -x. - if (number.is_zero()) - number = MathematicalValue { MathematicalValue::Symbol::NegativeZero }; - else - number.negate(); - } - - // 14. Return the Record { [[RoundedNumber]]: x, [[FormattedString]]: string }. - return { move(string), move(number) }; + return intl_object.formatter().format_to_decimal(number.to_value()); } // 15.5.4 PartitionNumberPattern ( numberFormat, x ), https://tc39.es/ecma402/#sec-partitionnumberpattern -Vector partition_number_pattern(VM& vm, NumberFormat& number_format, MathematicalValue number) +Vector<::Locale::NumberFormat::Partition> partition_number_pattern(NumberFormat const& number_format, MathematicalValue const& number) { - // 1. Let exponent be 0. - int exponent = 0; - - String formatted_string; - - // 2. If x is not-a-number, then - if (number.is_nan()) { - // a. Let n be an implementation- and locale-dependent (ILD) String value indicating the NaN value. - auto symbol = ::Locale::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), ::Locale::NumericSymbol::NaN).value_or("NaN"sv); - formatted_string = MUST(String::from_utf8(symbol)); - } - // 3. Else if x is positive-infinity, then - else if (number.is_positive_infinity()) { - // a. Let n be an ILD String value indicating positive infinity. - auto symbol = ::Locale::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), ::Locale::NumericSymbol::Infinity).value_or("infinity"sv); - formatted_string = MUST(String::from_utf8(symbol)); - } - // 4. Else if x is negative-infinity, then - else if (number.is_negative_infinity()) { - // a. Let n be an ILD String value indicating negative infinity. - // NOTE: The CLDR does not contain unique strings for negative infinity. The negative sign will - // be inserted by the pattern returned from GetNumberFormatPattern. - auto symbol = ::Locale::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), ::Locale::NumericSymbol::Infinity).value_or("infinity"sv); - formatted_string = MUST(String::from_utf8(symbol)); - } - // 5. Else, - else { - // a. If x is not negative-zero, - if (!number.is_negative_zero()) { - // i. Assert: x is a mathematical value. - VERIFY(number.is_mathematical_value()); - - // ii. If numberFormat.[[Style]] is "percent", let x be 100 × x. - if (number_format.style() == NumberFormat::Style::Percent) - number = number.multiplied_by(100); - - // iii. Let exponent be ComputeExponent(numberFormat, x). - exponent = compute_exponent(number_format, number); - - // iv. Let x be x × 10^-exponent. - number = number.multiplied_by_power(-exponent); - } - - // b. Let formatNumberResult be FormatNumericToString(numberFormat, x). - auto format_number_result = format_numeric_to_string(number_format, move(number)); - - // c. Let n be formatNumberResult.[[FormattedString]]. - formatted_string = move(format_number_result.formatted_string); - - // d. Let x be formatNumberResult.[[RoundedNumber]]. - number = move(format_number_result.rounded_number); - } - - ::Locale::NumberFormat found_pattern {}; - - // 6. Let pattern be GetNumberFormatPattern(numberFormat, x). - auto pattern = get_number_format_pattern(vm, number_format, number, found_pattern); - if (!pattern.has_value()) - return {}; - - // 7. Let result be a new empty List. - Vector result; - - // 8. Let patternParts be PartitionPattern(pattern). - auto pattern_parts = pattern->visit([](auto const& p) { return partition_pattern(p); }); - - // 9. For each Record { [[Type]], [[Value]] } patternPart of patternParts, do - for (auto& pattern_part : pattern_parts) { - // a. Let p be patternPart.[[Type]]. - auto part = pattern_part.type; - - // b. If p is "literal", then - if (part == "literal"sv) { - // i. Append a new Record { [[Type]]: "literal", [[Value]]: patternPart.[[Value]] } as the last element of result. - result.append({ "literal"sv, move(pattern_part.value) }); - } - - // c. Else if p is equal to "number", then - else if (part == "number"sv) { - // i. Let notationSubParts be PartitionNotationSubPattern(numberFormat, x, n, exponent). - auto notation_sub_parts = partition_notation_sub_pattern(number_format, number, formatted_string, exponent); - // ii. Append all elements of notationSubParts to result. - result.extend(move(notation_sub_parts)); - } - - // d. Else if p is equal to "plusSign", then - else if (part == "plusSign"sv) { - // i. Let plusSignSymbol be the ILND String representing the plus sign. - auto plus_sign_symbol = ::Locale::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), ::Locale::NumericSymbol::PlusSign).value_or("+"sv); - // ii. Append a new Record { [[Type]]: "plusSign", [[Value]]: plusSignSymbol } as the last element of result. - result.append({ "plusSign"sv, MUST(String::from_utf8(plus_sign_symbol)) }); - } - - // e. Else if p is equal to "minusSign", then - else if (part == "minusSign"sv) { - // i. Let minusSignSymbol be the ILND String representing the minus sign. - auto minus_sign_symbol = ::Locale::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), ::Locale::NumericSymbol::MinusSign).value_or("-"sv); - // ii. Append a new Record { [[Type]]: "minusSign", [[Value]]: minusSignSymbol } as the last element of result. - result.append({ "minusSign"sv, MUST(String::from_utf8(minus_sign_symbol)) }); - } - - // f. Else if p is equal to "percentSign" and numberFormat.[[Style]] is "percent", then - else if ((part == "percentSign"sv) && (number_format.style() == NumberFormat::Style::Percent)) { - // i. Let percentSignSymbol be the ILND String representing the percent sign. - auto percent_sign_symbol = ::Locale::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), ::Locale::NumericSymbol::PercentSign).value_or("%"sv); - // ii. Append a new Record { [[Type]]: "percentSign", [[Value]]: percentSignSymbol } as the last element of result. - result.append({ "percentSign"sv, MUST(String::from_utf8(percent_sign_symbol)) }); - } - - // g. Else if p is equal to "unitPrefix" and numberFormat.[[Style]] is "unit", then - // h. Else if p is equal to "unitSuffix" and numberFormat.[[Style]] is "unit", then - else if ((part.starts_with("unitIdentifier:"sv)) && (number_format.style() == NumberFormat::Style::Unit)) { - // Note: Our implementation combines "unitPrefix" and "unitSuffix" into one field, "unitIdentifier". - - auto identifier_index = part.substring_view("unitIdentifier:"sv.length()).to_number(); - VERIFY(identifier_index.has_value()); - - // i. Let unit be numberFormat.[[Unit]]. - // ii. Let unitDisplay be numberFormat.[[UnitDisplay]]. - // iii. Let mu be an ILD String value representing unit before x in unitDisplay form, which may depend on x in languages having different plural forms. - auto unit_identifier = found_pattern.identifiers[*identifier_index]; - - // iv. Append a new Record { [[Type]]: "unit", [[Value]]: mu } as the last element of result. - result.append({ "unit"sv, MUST(String::from_utf8(unit_identifier)) }); - } - - // i. Else if p is equal to "currencyCode" and numberFormat.[[Style]] is "currency", then - // j. Else if p is equal to "currencyPrefix" and numberFormat.[[Style]] is "currency", then - // k. Else if p is equal to "currencySuffix" and numberFormat.[[Style]] is "currency", then - // - // Note: Our implementation manipulates the format string to inject/remove spacing around the - // currency code during GetNumberFormatPattern so that we do not have to do currency - // display / plurality lookups more than once. - else if ((part == "currency"sv) && (number_format.style() == NumberFormat::Style::Currency)) { - auto currency = number_format.resolve_currency_display(); - result.append({ "currency"sv, MUST(String::from_utf8(currency)) }); - } - - // l. Else, - else { - // i. Let unknown be an ILND String based on x and p. - // ii. Append a new Record { [[Type]]: "unknown", [[Value]]: unknown } as the last element of result. - - // LibUnicode doesn't generate any "unknown" patterns. - VERIFY_NOT_REACHED(); - } - } - - // 10. Return result. - return result; -} - -static Vector separate_integer_into_groups(::Locale::NumberGroupings const& grouping_sizes, String integer, NumberFormat::UseGrouping use_grouping) -{ - auto utf8_integer = integer.code_points(); - if (utf8_integer.length() <= grouping_sizes.primary_grouping_size) - return { move(integer) }; - - size_t index = utf8_integer.length() - grouping_sizes.primary_grouping_size; - - switch (use_grouping) { - case NumberFormat::UseGrouping::Min2: - if (utf8_integer.length() < 5) - return { move(integer) }; - break; - - case NumberFormat::UseGrouping::Auto: - if (index < grouping_sizes.minimum_grouping_digits) - return { move(integer) }; - break; - - case NumberFormat::UseGrouping::Always: - break; - - default: - VERIFY_NOT_REACHED(); - } - - Vector groups; - - auto add_group = [&](size_t index, size_t length) { - length = utf8_integer.unicode_substring_view(index, length).byte_length(); - index = utf8_integer.byte_offset_of(index); - - auto group = MUST(integer.substring_from_byte_offset_with_shared_superstring(index, length)); - groups.prepend(move(group)); - }; - - add_group(index, grouping_sizes.primary_grouping_size); - - while (index > grouping_sizes.secondary_grouping_size) { - index -= grouping_sizes.secondary_grouping_size; - add_group(index, grouping_sizes.secondary_grouping_size); - } - - if (index > 0) - add_group(0, index); - - return groups; -} - -// 15.5.5 PartitionNotationSubPattern ( numberFormat, x, n, exponent ), https://tc39.es/ecma402/#sec-partitionnotationsubpattern -Vector partition_notation_sub_pattern(NumberFormat& number_format, MathematicalValue const& number, String formatted_string, int exponent) -{ - // 1. Let result be a new empty List. - Vector result; - - auto grouping_sizes = ::Locale::get_number_system_groupings(number_format.data_locale(), number_format.numbering_system()); - if (!grouping_sizes.has_value()) - return {}; - - // 2. If x is not-a-number, then - if (number.is_nan()) { - // a. Append a new Record { [[Type]]: "nan", [[Value]]: n } as the last element of result. - result.append({ "nan"sv, move(formatted_string) }); - } - // 3. Else if x is positive-infinity or negative-infinity, then - else if (number.is_positive_infinity() || number.is_negative_infinity()) { - // a. Append a new Record { [[Type]]: "infinity", [[Value]]: n } as the last element of result. - result.append({ "infinity"sv, move(formatted_string) }); - } - // 4. Else, - else { - // a. Let notationSubPattern be GetNotationSubPattern(numberFormat, exponent). - auto notation_sub_pattern = get_notation_sub_pattern(number_format, exponent); - if (!notation_sub_pattern.has_value()) - return {}; - - // b. Let patternParts be PartitionPattern(notationSubPattern). - auto pattern_parts = partition_pattern(*notation_sub_pattern); - - // c. For each Record { [[Type]], [[Value]] } patternPart of patternParts, do - for (auto& pattern_part : pattern_parts) { - // i. Let p be patternPart.[[Type]]. - auto part = pattern_part.type; - - // ii. If p is "literal", then - if (part == "literal"sv) { - // 1. Append a new Record { [[Type]]: "literal", [[Value]]: patternPart.[[Value]] } as the last element of result. - result.append({ "literal"sv, move(pattern_part.value) }); - } - // iii. Else if p is equal to "number", then - else if (part == "number"sv) { - // 1. If the numberFormat.[[NumberingSystem]] matches one of the values in the "Numbering System" column of Table 14 below, then - // a. Let digits be a List whose elements are the code points specified in the "Digits" column of the matching row in Table 14. - // b. Assert: The length of digits is 10. - // c. Let transliterated be the empty String. - // d. Let len be the length of n. - // e. Let position be 0. - // f. Repeat, while position < len, - // i. Let c be the code unit at index position within n. - // ii. If 0x0030 ≤ c ≤ 0x0039, then - // i. NOTE: c is an ASCII digit. - // ii. Let i be c - 0x0030. - // iii. Set c to CodePointsToString(« digits[i] »). - // iii. Set transliterated to the string-concatenation of transliterated and c. - // iv. Set position to position + 1. - // g. Set n to transliterated. - // 2. Else use an implementation dependent algorithm to map n to the appropriate representation of n in the given numbering system. - formatted_string = ::Locale::replace_digits_for_number_system(number_format.numbering_system(), formatted_string); - - // 3. Let decimalSepIndex be StringIndexOf(n, ".", 0). - auto decimal_sep_index = formatted_string.find_byte_offset('.'); - - String integer; - Optional fraction; - - // 4. If decimalSepIndex > 0, then - if (decimal_sep_index.has_value() && (*decimal_sep_index > 0)) { - // a. Let integer be the substring of n from position 0, inclusive, to position decimalSepIndex, exclusive. - integer = MUST(formatted_string.substring_from_byte_offset_with_shared_superstring(0, *decimal_sep_index)); - - // b. Let fraction be the substring of n from position decimalSepIndex, exclusive, to the end of n. - fraction = MUST(formatted_string.substring_from_byte_offset_with_shared_superstring(*decimal_sep_index + 1)); - } - // 5. Else, - else { - // a. Let integer be n. - integer = move(formatted_string); - // b. Let fraction be undefined. - } - - // 6. If the numberFormat.[[UseGrouping]] is false, then - if (number_format.use_grouping() == NumberFormat::UseGrouping::False) { - // a. Append a new Record { [[Type]]: "integer", [[Value]]: integer } as the last element of result. - result.append({ "integer"sv, move(integer) }); - } - // 7. Else, - else { - // a. Let groupSepSymbol be the implementation-, locale-, and numbering system-dependent (ILND) String representing the grouping separator. - auto group_sep_symbol = ::Locale::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), ::Locale::NumericSymbol::Group).value_or(","sv); - - // b. Let groups be a List whose elements are, in left to right order, the substrings defined by ILND set of locations within the integer, which may depend on the value of numberFormat.[[UseGrouping]]. - auto groups = separate_integer_into_groups(*grouping_sizes, move(integer), number_format.use_grouping()); - - // c. Assert: The number of elements in groups List is greater than 0. - VERIFY(!groups.is_empty()); - - // d. Repeat, while groups List is not empty, - while (!groups.is_empty()) { - // i. Remove the first element from groups and let integerGroup be the value of that element. - auto integer_group = groups.take_first(); - - // ii. Append a new Record { [[Type]]: "integer", [[Value]]: integerGroup } as the last element of result. - result.append({ "integer"sv, move(integer_group) }); - - // iii. If groups List is not empty, then - if (!groups.is_empty()) { - // i. Append a new Record { [[Type]]: "group", [[Value]]: groupSepSymbol } as the last element of result. - result.append({ "group"sv, MUST(String::from_utf8(group_sep_symbol)) }); - } - } - } - - // 8. If fraction is not undefined, then - if (fraction.has_value()) { - // a. Let decimalSepSymbol be the ILND String representing the decimal separator. - auto decimal_sep_symbol = ::Locale::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), ::Locale::NumericSymbol::Decimal).value_or("."sv); - // b. Append a new Record { [[Type]]: "decimal", [[Value]]: decimalSepSymbol } as the last element of result. - result.append({ "decimal"sv, MUST(String::from_utf8(decimal_sep_symbol)) }); - // c. Append a new Record { [[Type]]: "fraction", [[Value]]: fraction } as the last element of result. - result.append({ "fraction"sv, fraction.release_value() }); - } - } - // iv. Else if p is equal to "compactSymbol", then - // v. Else if p is equal to "compactName", then - else if (part.starts_with("compactIdentifier:"sv)) { - // Note: Our implementation combines "compactSymbol" and "compactName" into one field, "compactIdentifier". - - auto identifier_index = part.substring_view("compactIdentifier:"sv.length()).to_number(); - VERIFY(identifier_index.has_value()); - - // 1. Let compactSymbol be an ILD string representing exponent in short form, which may depend on x in languages having different plural forms. The implementation must be able to provide this string, or else the pattern would not have a "{compactSymbol}" placeholder. - auto compact_identifier = number_format.compact_format().identifiers[*identifier_index]; - - // 2. Append a new Record { [[Type]]: "compact", [[Value]]: compactSymbol } as the last element of result. - result.append({ "compact"sv, MUST(String::from_utf8(compact_identifier)) }); - } - // vi. Else if p is equal to "scientificSeparator", then - else if (part == "scientificSeparator"sv) { - // 1. Let scientificSeparator be the ILND String representing the exponent separator. - auto scientific_separator = ::Locale::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), ::Locale::NumericSymbol::Exponential).value_or("E"sv); - // 2. Append a new Record { [[Type]]: "exponentSeparator", [[Value]]: scientificSeparator } as the last element of result. - result.append({ "exponentSeparator"sv, MUST(String::from_utf8(scientific_separator)) }); - } - // vii. Else if p is equal to "scientificExponent", then - else if (part == "scientificExponent"sv) { - // 1. If exponent < 0, then - if (exponent < 0) { - // a. Let minusSignSymbol be the ILND String representing the minus sign. - auto minus_sign_symbol = ::Locale::get_number_system_symbol(number_format.data_locale(), number_format.numbering_system(), ::Locale::NumericSymbol::MinusSign).value_or("-"sv); - - // b. Append a new Record { [[Type]]: "exponentMinusSign", [[Value]]: minusSignSymbol } as the last element of result. - result.append({ "exponentMinusSign"sv, MUST(String::from_utf8(minus_sign_symbol)) }); - - // c. Let exponent be -exponent. - exponent *= -1; - } - - // 2. Let exponentResult be ToRawFixed(exponent, 0, 0, 1, undefined). - auto exponent_value = MathematicalValue { static_cast(exponent) }; - auto exponent_result = to_raw_fixed(exponent_value, 0, 0, 1, {}); - - // FIXME: The spec does not say to do this, but all of major engines perform this replacement. - // Without this, formatting with non-Latin numbering systems will produce non-localized results. - exponent_result.formatted_string = ::Locale::replace_digits_for_number_system(number_format.numbering_system(), exponent_result.formatted_string); - - // 3. Append a new Record { [[Type]]: "exponentInteger", [[Value]]: exponentResult.[[FormattedString]] } as the last element of result. - result.append({ "exponentInteger"sv, move(exponent_result.formatted_string) }); - } - // viii. Else, - else { - // 1. Let unknown be an ILND String based on x and p. - // 2. Append a new Record { [[Type]]: "unknown", [[Value]]: unknown } as the last element of result. - - // LibUnicode doesn't generate any "unknown" patterns. - VERIFY_NOT_REACHED(); - } - } - } - - // 5. Return result. - return result; + return number_format.formatter().format_to_parts(number.to_value()); } // 15.5.6 FormatNumeric ( numberFormat, x ), https://tc39.es/ecma402/#sec-formatnumber -String format_numeric(VM& vm, NumberFormat& number_format, MathematicalValue number) +String format_numeric(NumberFormat const& number_format, MathematicalValue const& number) { // 1. Let parts be ? PartitionNumberPattern(numberFormat, x). - auto parts = partition_number_pattern(vm, number_format, move(number)); - // 2. Let result be the empty String. - StringBuilder result; - // 3. For each Record { [[Type]], [[Value]] } part in parts, do - for (auto& part : parts) { - // a. Set result to the string-concatenation of result and part.[[Value]]. - result.append(part.value); - } - + // a. Set result to the string-concatenation of result and part.[[Value]]. // 4. Return result. - return MUST(result.to_string()); + return number_format.formatter().format(number.to_value()); } // 15.5.7 FormatNumericToParts ( numberFormat, x ), https://tc39.es/ecma402/#sec-formatnumbertoparts -NonnullGCPtr format_numeric_to_parts(VM& vm, NumberFormat& number_format, MathematicalValue number) +NonnullGCPtr format_numeric_to_parts(VM& vm, NumberFormat const& number_format, MathematicalValue const& number) { auto& realm = *vm.current_realm(); // 1. Let parts be ? PartitionNumberPattern(numberFormat, x). - auto parts = partition_number_pattern(vm, number_format, move(number)); + auto parts = partition_number_pattern(number_format, number); // 2. Let result be ! ArrayCreate(0). auto result = MUST(Array::create(realm, 0)); @@ -974,683 +184,6 @@ NonnullGCPtr format_numeric_to_parts(VM& vm, NumberFormat& number_format, return result; } -static String cut_trailing_zeroes(StringView string, int cut) -{ - // These steps are exactly the same between ToRawPrecision and ToRawFixed. - - // Repeat, while cut > 0 and the last code unit of m is 0x0030 (DIGIT ZERO), - while ((cut > 0) && string.ends_with('0')) { - // Remove the last code unit from m. - string = string.substring_view(0, string.length() - 1); - - // Decrease cut by 1. - --cut; - } - - // If the last code unit of m is 0x002E (FULL STOP), then - if (string.ends_with('.')) { - // Remove the last code unit from m. - string = string.substring_view(0, string.length() - 1); - } - - return MUST(String::from_utf8(string)); -} - -enum class PreferredResult { - LessThanNumber, - GreaterThanNumber, -}; - -struct RawPrecisionResult { - MathematicalValue number; - int exponent { 0 }; - MathematicalValue rounded; -}; - -// ToRawPrecisionFn, https://tc39.es/ecma402/#eqn-ToRawPrecisionFn -static RawPrecisionResult to_raw_precision_function(MathematicalValue const& number, int precision, PreferredResult mode) -{ - RawPrecisionResult result {}; - result.exponent = number.logarithmic_floor(); - - if (number.is_number()) { - result.number = number.divided_by_power(result.exponent - precision + 1); - - switch (mode) { - case PreferredResult::LessThanNumber: - result.number = MathematicalValue { floor(result.number.as_number()) }; - break; - case PreferredResult::GreaterThanNumber: - result.number = MathematicalValue { ceil(result.number.as_number()) }; - break; - } - } else { - // NOTE: In order to round the BigInt to the proper precision, this computation is initially off by a - // factor of 10. This lets us inspect the ones digit and then round up if needed. - result.number = number.divided_by_power(result.exponent - precision); - - // FIXME: Can we do this without string conversion? - auto digits = result.number.to_string(); - auto digit = digits.bytes_as_string_view().substring_view(digits.bytes_as_string_view().length() - 1); - - result.number = result.number.divided_by(10); - - if (mode == PreferredResult::GreaterThanNumber && digit.to_number().value() != 0) - result.number = result.number.plus(1); - } - - result.rounded = result.number.multiplied_by_power(result.exponent - precision + 1); - return result; -} - -// 15.5.8 ToRawPrecision ( x, minPrecision, maxPrecision ), https://tc39.es/ecma402/#sec-torawprecision -RawFormatResult to_raw_precision(MathematicalValue const& number, int min_precision, int max_precision, NumberFormat::UnsignedRoundingMode unsigned_rounding_mode) -{ - RawFormatResult result {}; - - // 1. Let p be maxPrecision. - int precision = max_precision; - int exponent = 0; - - // 2. If x = 0, then - if (number.is_zero()) { - // a. Let m be the String consisting of p occurrences of the code unit 0x0030 (DIGIT ZERO). - result.formatted_string = MUST(String::repeated('0', precision)); - - // b. Let e be 0. - exponent = 0; - - // c. Let xFinal be 0. - result.rounded_number = MathematicalValue { 0.0 }; - } - // 3. Else, - else { - // a. Let n1 and e1 each be an integer and r1 a mathematical value, with r1 = ToRawPrecisionFn(n1, e1, p), such that r1 ≤ x and r1 is maximized. - auto [number1, exponent1, rounded1] = to_raw_precision_function(number, precision, PreferredResult::LessThanNumber); - - // b. Let n2 and e2 each be an integer and r2 a mathematical value, with r2 = ToRawPrecisionFn(n2, e2, p), such that r2 ≥ x and r2 is minimized. - auto [number2, exponent2, rounded2] = to_raw_precision_function(number, precision, PreferredResult::GreaterThanNumber); - - // c. Let r be ApplyUnsignedRoundingMode(x, r1, r2, unsignedRoundingMode). - auto rounded = apply_unsigned_rounding_mode(number, rounded1, rounded2, unsigned_rounding_mode); - - MathematicalValue n; - - // d. If r is r1, then - if (rounded == RoundingDecision::LowerValue) { - // i. Let n be n1. - n = move(number1); - - // ii. Let e be e1. - exponent = exponent1; - - // iii. Let xFinal be r1. - result.rounded_number = move(rounded1); - } - // e. Else, - else { - // i. Let n be n2. - n = move(number2); - - // ii. Let e be e2. - exponent = exponent2; - - // iii. Let xFinal be r2. - result.rounded_number = move(rounded2); - } - - // f. Let m be the String consisting of the digits of the decimal representation of n (in order, with no leading zeroes). - result.formatted_string = n.to_string(); - } - - // 4. If e ≥ (p – 1), then - if (exponent >= (precision - 1)) { - // a. Set m to the string-concatenation of m and e - p + 1 occurrences of the code unit 0x0030 (DIGIT ZERO). - result.formatted_string = MUST(String::formatted( - "{}{}", - result.formatted_string, - MUST(String::repeated('0', exponent - precision + 1)))); - - // b. Let int be e + 1. - result.digits = exponent + 1; - } - // 5. Else if e ≥ 0, then - else if (exponent >= 0) { - // a. Set m to the string-concatenation of the first e + 1 code units of m, the code unit 0x002E (FULL STOP), and the remaining p - (e + 1) code units of m. - result.formatted_string = MUST(String::formatted( - "{}.{}", - result.formatted_string.bytes_as_string_view().substring_view(0, exponent + 1), - result.formatted_string.bytes_as_string_view().substring_view(exponent + 1))); - - // b. Let int be e + 1. - result.digits = exponent + 1; - } - // 6. Else, - else { - // a. Assert: e < 0. - // b. Set m to the string-concatenation of "0.", -(e + 1) occurrences of the code unit 0x0030 (DIGIT ZERO), and m. - result.formatted_string = MUST(String::formatted( - "0.{}{}", - MUST(String::repeated('0', -1 * (exponent + 1))), - result.formatted_string)); - - // c. Let int be 1. - result.digits = 1; - } - - // 7. If m contains the code unit 0x002E (FULL STOP) and maxPrecision > minPrecision, then - if (result.formatted_string.contains('.') && (max_precision > min_precision)) { - // a. Let cut be maxPrecision – minPrecision. - int cut = max_precision - min_precision; - - // Steps 8b-8c are implemented by cut_trailing_zeroes. - result.formatted_string = cut_trailing_zeroes(result.formatted_string, cut); - } - - // 8. Return the Record { [[FormattedString]]: m, [[RoundedNumber]]: xFinal, [[IntegerDigitsCount]]: int, [[RoundingMagnitude]]: e–p+1 }. - result.rounding_magnitude = exponent - precision + 1; - return result; -} - -struct RawFixedResult { - MathematicalValue number; - MathematicalValue rounded; -}; - -// ToRawFixedFn, https://tc39.es/ecma402/#eqn-ToRawFixedFn -static RawFixedResult to_raw_fixed_function(MathematicalValue const& number, int fraction, int rounding_increment, PreferredResult mode) -{ - RawFixedResult result {}; - - if (number.is_number()) { - result.number = number.multiplied_by_power(fraction); - - switch (mode) { - case PreferredResult::LessThanNumber: - result.number = MathematicalValue { floor(result.number.as_number()) }; - break; - case PreferredResult::GreaterThanNumber: - result.number = MathematicalValue { ceil(result.number.as_number()) }; - break; - } - } else { - // NOTE: In order to round the BigInt to the proper precision, this computation is initially off by a - // factor of 10. This lets us inspect the ones digit and then round up if needed. - result.number = number.multiplied_by_power(fraction - 1); - - // FIXME: Can we do this without string conversion? - auto digits = result.number.to_string(); - auto digit = digits.bytes_as_string_view().substring_view(digits.bytes_as_string_view().length() - 1); - - result.number = result.number.multiplied_by(10); - - if (mode == PreferredResult::GreaterThanNumber && digit.to_number().value() != 0) - result.number = result.number.plus(1); - } - - while (!result.number.modulo_is_zero(rounding_increment)) { - switch (mode) { - case PreferredResult::LessThanNumber: - result.number = result.number.minus(1); - break; - case PreferredResult::GreaterThanNumber: - result.number = result.number.plus(1); - break; - } - } - - result.rounded = result.number.divided_by_power(fraction); - return result; -} - -// 15.5.9 ToRawFixed ( x, minInteger, minFraction, maxFraction ), https://tc39.es/ecma402/#sec-torawfixed -RawFormatResult to_raw_fixed(MathematicalValue const& number, int min_fraction, int max_fraction, int rounding_increment, NumberFormat::UnsignedRoundingMode unsigned_rounding_mode) -{ - RawFormatResult result {}; - - // 1. Let f be maxFraction. - int fraction = max_fraction; - - // 2. Let n1 be an integer and r1 a mathematical value, with r1 = ToRawFixedFn(n1, f), such that n1 modulo roundingIncrement = 0, r1 ≤ x, and r1 is maximized. - auto [number1, rounded1] = to_raw_fixed_function(number, fraction, rounding_increment, PreferredResult::LessThanNumber); - - // 3. Let n2 be an integer and r2 a mathematical value, with r2 = ToRawFixedFn(n2, f), such that n2 modulo roundingIncrement = 0, r2 ≥ x, and r2 is minimized. - auto [number2, rounded2] = to_raw_fixed_function(number, fraction, rounding_increment, PreferredResult::GreaterThanNumber); - - // 4. Let r be ApplyUnsignedRoundingMode(x, r1, r2, unsignedRoundingMode). - auto rounded = apply_unsigned_rounding_mode(number, rounded1, rounded2, unsigned_rounding_mode); - - MathematicalValue n; - - // 5. If r is r1, then - if (rounded == RoundingDecision::LowerValue) { - // a. Let n be n1. - n = move(number1); - - // b. Let xFinal be r1. - result.rounded_number = move(rounded1); - } - // 6. Else, - else { - // a. Let n be n2. - n = move(number2); - - // b. Let xFinal be r2. - result.rounded_number = move(rounded2); - } - - // 7. If n = 0, let m be "0". Otherwise, let m be the String consisting of the digits of the decimal representation of n (in order, with no leading zeroes). - result.formatted_string = n.is_zero() ? "0"_string : n.to_string(); - - // 8. If f ≠ 0, then - if (fraction != 0) { - // a. Let k be the length of m. - auto decimals = result.formatted_string.bytes_as_string_view().length(); - - // b. If k ≤ f, then - if (decimals <= static_cast(fraction)) { - // i. Let z be the String value consisting of f + 1 - k occurrences of the code unit 0x0030 (DIGIT ZERO). - auto zeroes = MUST(String::repeated('0', fraction + 1 - decimals)); - - // ii. Let m be the string-concatenation of z and m. - result.formatted_string = MUST(String::formatted("{}{}", zeroes, result.formatted_string)); - - // iii. Let k be f + 1. - decimals = fraction + 1; - } - - // c. Let a be the first k - f code units of m, and let b be the remaining f code units of m. - auto a = result.formatted_string.bytes_as_string_view().substring_view(0, decimals - fraction); - auto b = result.formatted_string.bytes_as_string_view().substring_view(decimals - fraction, fraction); - - // d. Let m be the string-concatenation of a, ".", and b. - result.formatted_string = MUST(String::formatted("{}.{}", a, b)); - - // e. Let int be the length of a. - result.digits = a.length(); - } - // 9. Else, let int be the length of m. - else { - result.digits = result.formatted_string.bytes_as_string_view().length(); - } - - // 10. Let cut be maxFraction – minFraction. - int cut = max_fraction - min_fraction; - - // Steps 11-12 are implemented by cut_trailing_zeroes. - result.formatted_string = cut_trailing_zeroes(result.formatted_string, cut); - - // 13. Return the Record { [[FormattedString]]: m, [[RoundedNumber]]: xFinal, [[IntegerDigitsCount]]: int, [[RoundingMagnitude]]: –f }. - result.rounding_magnitude = -fraction; - return result; -} - -enum class NumberCategory { - NegativeNonZero, - NegativeZero, - PositiveNonZero, - PositiveZero, -}; - -// 15.5.11 GetNumberFormatPattern ( numberFormat, x ), https://tc39.es/ecma402/#sec-getnumberformatpattern -Optional> get_number_format_pattern(VM& vm, NumberFormat& number_format, MathematicalValue const& number, ::Locale::NumberFormat& found_pattern) -{ - // 1. Let localeData be %NumberFormat%.[[LocaleData]]. - // 2. Let dataLocale be numberFormat.[[DataLocale]]. - // 3. Let dataLocaleData be localeData.[[]]. - // 4. Let patterns be dataLocaleData.[[patterns]]. - // 5. Assert: patterns is a Record (see 15.3.3). - Optional<::Locale::NumberFormat> patterns; - - // 6. Let style be numberFormat.[[Style]]. - switch (number_format.style()) { - // 7. If style is "percent", then - case NumberFormat::Style::Percent: - // a. Let patterns be patterns.[[percent]]. - patterns = ::Locale::get_standard_number_system_format(number_format.data_locale(), number_format.numbering_system(), ::Locale::StandardNumberFormatType::Percent); - break; - - // 8. Else if style is "unit", then - case NumberFormat::Style::Unit: { - // a. Let unit be numberFormat.[[Unit]]. - // b. Let unitDisplay be numberFormat.[[UnitDisplay]]. - // c. Let patterns be patterns.[[unit]]. - // d. If patterns doesn't have a field [[]], then - // i. Let unit be "fallback". - // e. Let patterns be patterns.[[]]. - // f. Let patterns be patterns.[[]]. - auto formats = ::Locale::get_unit_formats(number_format.data_locale(), number_format.unit(), number_format.unit_display()); - auto plurality = resolve_plural(number_format, ::Locale::PluralForm::Cardinal, number.to_value(vm)); - - if (auto it = formats.find_if([&](auto& p) { return p.plurality == plurality.plural_category; }); it != formats.end()) - patterns = move(*it); - - break; - } - - // 9. Else if style is "currency", then - case NumberFormat::Style::Currency: - // a. Let currency be numberFormat.[[Currency]]. - // b. Let currencyDisplay be numberFormat.[[CurrencyDisplay]]. - // c. Let currencySign be numberFormat.[[CurrencySign]]. - // d. Let patterns be patterns.[[currency]]. - // e. If patterns doesn't have a field [[]], then - // i. Let currency be "fallback". - // 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 formats = ::Locale::get_compact_number_system_formats(number_format.data_locale(), number_format.numbering_system(), ::Locale::CompactNumberFormatType::CurrencyUnit); - auto plurality = resolve_plural(number_format, ::Locale::PluralForm::Cardinal, number.to_value(vm)); - - if (auto it = formats.find_if([&](auto& p) { return p.plurality == plurality.plural_category; }); it != formats.end()) { - patterns = move(*it); - break; - } - } - - switch (number_format.currency_sign()) { - case NumberFormat::CurrencySign::Standard: - patterns = ::Locale::get_standard_number_system_format(number_format.data_locale(), number_format.numbering_system(), ::Locale::StandardNumberFormatType::Currency); - break; - case NumberFormat::CurrencySign::Accounting: - patterns = ::Locale::get_standard_number_system_format(number_format.data_locale(), number_format.numbering_system(), ::Locale::StandardNumberFormatType::Accounting); - break; - } - - break; - - // 10. Else, - case NumberFormat::Style::Decimal: - // a. Assert: style is "decimal". - // b. Let patterns be patterns.[[decimal]]. - patterns = ::Locale::get_standard_number_system_format(number_format.data_locale(), number_format.numbering_system(), ::Locale::StandardNumberFormatType::Decimal); - break; - - default: - VERIFY_NOT_REACHED(); - } - - if (!patterns.has_value()) - return {}; - - NumberCategory category; - - // 11. If x is negative-infinity, then - if (number.is_negative_infinity()) { - // a. Let category be negative-nonzero. - category = NumberCategory::NegativeNonZero; - } - // 12. Else if x is negative-zero, then - else if (number.is_negative_zero()) { - // a. Let category be negative-zero. - category = NumberCategory::NegativeZero; - } - // 13. Else if x is not-a-number, then - else if (number.is_nan()) { - // a. Let category be positive-zero. - category = NumberCategory::PositiveZero; - } - // 14. Else if x is positive-infinity, then - else if (number.is_positive_infinity()) { - // a. Let category be positive-nonzero. - category = NumberCategory::PositiveNonZero; - } - // 15. Else, - else { - // a. Assert: x is a mathematical value. - VERIFY(number.is_mathematical_value()); - - // b. If x < 0, then - if (number.is_negative()) { - // i. Let category be negative-nonzero. - category = NumberCategory::NegativeNonZero; - } - // c. Else if x > 0, then - else if (number.is_positive()) { - // i. Let category be positive-nonzero. - category = NumberCategory::PositiveNonZero; - } - // d. Else, - else { - // i. Let category be positive-zero. - category = NumberCategory::PositiveZero; - } - } - - StringView pattern; - - // 16. Let signDisplay be numberFormat.[[SignDisplay]]. - switch (number_format.sign_display()) { - // 17. If signDisplay is "never", then - case NumberFormat::SignDisplay::Never: - // a. Let pattern be patterns.[[zeroPattern]]. - pattern = patterns->zero_format; - break; - - // 18. Else if signDisplay is "auto", then - case NumberFormat::SignDisplay::Auto: - // a. If category is positive-nonzero or positive-zero, then - if (category == NumberCategory::PositiveNonZero || category == NumberCategory::PositiveZero) { - // i. Let pattern be patterns.[[zeroPattern]]. - pattern = patterns->zero_format; - } - // b. Else, - else { - // i. Let pattern be patterns.[[negativePattern]]. - pattern = patterns->negative_format; - } - break; - - // 19. Else if signDisplay is "always", then - case NumberFormat::SignDisplay::Always: - // a. If category is positive-nonzero or positive-zero, then - if (category == NumberCategory::PositiveNonZero || category == NumberCategory::PositiveZero) { - // i. Let pattern be patterns.[[positivePattern]]. - pattern = patterns->positive_format; - } - // b. Else, - else { - // i. Let pattern be patterns.[[negativePattern]]. - pattern = patterns->negative_format; - } - break; - - // 20. Else if signDisplay is "exceptZero", then - case NumberFormat::SignDisplay::ExceptZero: - // a. If category is positive-zero or negative-zero, then - if (category == NumberCategory::PositiveZero || category == NumberCategory::NegativeZero) { - // i. Let pattern be patterns.[[zeroPattern]]. - pattern = patterns->zero_format; - } - // b. Else if category is positive-nonzero, then - else if (category == NumberCategory::PositiveNonZero) { - // i. Let pattern be patterns.[[positivePattern]]. - pattern = patterns->positive_format; - } - // c. Else, - else { - // i. Let pattern be patterns.[[negativePattern]]. - pattern = patterns->negative_format; - } - break; - - // 21. Else, - case NumberFormat::SignDisplay::Negative: - // a. Assert: signDisplay is "negative". - // b. If category is negative-nonzero, then - if (category == NumberCategory::NegativeNonZero) { - // i. Let pattern be patterns.[[negativePattern]]. - pattern = patterns->negative_format; - } - // c. Else, - else { - // i. Let pattern be patterns.[[zeroPattern]]. - pattern = patterns->zero_format; - } - break; - - default: - VERIFY_NOT_REACHED(); - } - - found_pattern = patterns.release_value(); - - // 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) { - auto modified_pattern = ::Locale::augment_currency_format_pattern(number_format.resolve_currency_display(), pattern); - if (modified_pattern.has_value()) - return modified_pattern.release_value(); - } - - // 22. Return pattern. - return pattern; -} - -// 15.5.12 GetNotationSubPattern ( numberFormat, exponent ), https://tc39.es/ecma402/#sec-getnotationsubpattern -Optional get_notation_sub_pattern(NumberFormat& number_format, int exponent) -{ - // 1. Let localeData be %NumberFormat%.[[LocaleData]]. - // 2. Let dataLocale be numberFormat.[[DataLocale]]. - // 3. Let dataLocaleData be localeData.[[]]. - // 4. Let notationSubPatterns be dataLocaleData.[[notationSubPatterns]]. - // 5. Assert: notationSubPatterns is a Record (see 15.3.3). - - // 6. Let notation be numberFormat.[[Notation]]. - auto notation = number_format.notation(); - - // 7. If notation is "scientific" or notation is "engineering", then - if ((notation == NumberFormat::Notation::Scientific) || (notation == NumberFormat::Notation::Engineering)) { - // a. Return notationSubPatterns.[[scientific]]. - auto notation_sub_patterns = ::Locale::get_standard_number_system_format(number_format.data_locale(), number_format.numbering_system(), ::Locale::StandardNumberFormatType::Scientific); - if (!notation_sub_patterns.has_value()) - return {}; - - return notation_sub_patterns->zero_format; - } - // 8. Else if exponent is not 0, then - else if (exponent != 0) { - // a. Assert: notation is "compact". - VERIFY(notation == NumberFormat::Notation::Compact); - - // b. Let compactDisplay be numberFormat.[[CompactDisplay]]. - // c. Let compactPatterns be notationSubPatterns.[[compact]].[[]]. - // d. Return compactPatterns.[[]]. - if (number_format.has_compact_format()) - return number_format.compact_format().zero_format; - } - - // 9. Else, - // a. Return "{number}". - return "{number}"sv; -} - -// 15.5.13 ComputeExponent ( numberFormat, x ), https://tc39.es/ecma402/#sec-computeexponent -int compute_exponent(NumberFormat& number_format, MathematicalValue number) -{ - // 1. If x = 0, then - if (number.is_zero()) { - // a. Return 0. - return 0; - } - - // 2. If x < 0, then - if (number.is_negative()) { - // a. Let x = -x. - number.negate(); - } - - // 3. Let magnitude be the base 10 logarithm of x rounded down to the nearest integer. - int magnitude = number.logarithmic_floor(); - - // 4. Let exponent be ComputeExponentForMagnitude(numberFormat, magnitude). - int exponent = compute_exponent_for_magnitude(number_format, magnitude); - - // 5. Let x be x × 10^(-exponent). - number = number.multiplied_by_power(-exponent); - - // 6. Let formatNumberResult be FormatNumericToString(numberFormat, x). - auto format_number_result = format_numeric_to_string(number_format, move(number)); - - // 7. If formatNumberResult.[[RoundedNumber]] = 0, then - if (format_number_result.rounded_number.is_zero()) { - // a. Return exponent. - return exponent; - } - - // 8. Let newMagnitude be the base 10 logarithm of formatNumberResult.[[RoundedNumber]] rounded down to the nearest integer. - int new_magnitude = format_number_result.rounded_number.logarithmic_floor(); - - // 9. If newMagnitude is magnitude - exponent, then - if (new_magnitude == magnitude - exponent) { - // a. Return exponent. - return exponent; - } - - // 10. Return ComputeExponentForMagnitude(numberFormat, magnitude + 1). - return compute_exponent_for_magnitude(number_format, magnitude + 1); -} - -// 15.5.14 ComputeExponentForMagnitude ( numberFormat, magnitude ), https://tc39.es/ecma402/#sec-computeexponentformagnitude -int compute_exponent_for_magnitude(NumberFormat& number_format, int magnitude) -{ - // 1. Let notation be numberFormat.[[Notation]]. - switch (number_format.notation()) { - // 2. If notation is "standard", then - case NumberFormat::Notation::Standard: - // a. Return 0. - return 0; - - // 3. Else if notation is "scientific", then - case NumberFormat::Notation::Scientific: - // a. Return magnitude. - return magnitude; - - // 4. Else if notation is "engineering", then - case NumberFormat::Notation::Engineering: { - // a. Let thousands be the greatest integer that is not greater than magnitude / 3. - double thousands = floor(static_cast(magnitude) / 3.0); - - // b. Return thousands × 3. - return static_cast(thousands) * 3; - } - - // 5. Else, - case NumberFormat::Notation::Compact: { - // a. Assert: notation is "compact". - VERIFY(number_format.has_compact_display()); - - // b. Let exponent be an implementation- and locale-dependent (ILD) integer by which to scale a number of the given magnitude in compact notation for the current locale. - // c. Return exponent. - auto compact_format_type = number_format.compact_display() == NumberFormat::CompactDisplay::Short || number_format.style() == NumberFormat::Style::Currency - ? ::Locale::CompactNumberFormatType::DecimalShort - : ::Locale::CompactNumberFormatType::DecimalLong; - - auto format_rules = ::Locale::get_compact_number_system_formats(number_format.data_locale(), number_format.numbering_system(), compact_format_type); - ::Locale::NumberFormat const* best_number_format = nullptr; - - for (auto const& format_rule : format_rules) { - if (format_rule.magnitude > magnitude) - break; - best_number_format = &format_rule; - } - - if (best_number_format == nullptr) - return 0; - - number_format.set_compact_format(*best_number_format); - return best_number_format->exponent; - } - - default: - VERIFY_NOT_REACHED(); - } -} - // 15.5.16 ToIntlMathematicalValue ( value ), https://tc39.es/ecma402/#sec-tointlmathematicalvalue ThrowCompletionOr to_intl_mathematical_value(VM& vm, Value value) { @@ -1695,101 +228,6 @@ ThrowCompletionOr to_intl_mathematical_value(VM& vm, Value va return mathematical_value; } -// 15.5.17 GetUnsignedRoundingMode ( roundingMode, isNegative ), https://tc39.es/ecma402/#sec-getunsignedroundingmode -NumberFormat::UnsignedRoundingMode get_unsigned_rounding_mode(NumberFormat::RoundingMode rounding_mode, bool is_negative) -{ - // 1. If isNegative is true, return the specification type in the third column of Table 15 where the first column is roundingMode and the second column is "negative". - // 2. Else, return the specification type in the third column of Table 15 where the first column is roundingMode and the second column is "positive". - - // Table 15: Conversion from rounding mode to unsigned rounding mode, https://tc39.es/ecma402/#table-intl-unsigned-rounding-modes - switch (rounding_mode) { - case NumberFormat::RoundingMode::Ceil: - return is_negative ? NumberFormat::UnsignedRoundingMode::Zero : NumberFormat::UnsignedRoundingMode::Infinity; - case NumberFormat::RoundingMode::Floor: - return is_negative ? NumberFormat::UnsignedRoundingMode::Infinity : NumberFormat::UnsignedRoundingMode::Zero; - case NumberFormat::RoundingMode::Expand: - return NumberFormat::UnsignedRoundingMode::Infinity; - case NumberFormat::RoundingMode::Trunc: - return NumberFormat::UnsignedRoundingMode::Zero; - case NumberFormat::RoundingMode::HalfCeil: - return is_negative ? NumberFormat::UnsignedRoundingMode::HalfZero : NumberFormat::UnsignedRoundingMode::HalfInfinity; - case NumberFormat::RoundingMode::HalfFloor: - return is_negative ? NumberFormat::UnsignedRoundingMode::HalfInfinity : NumberFormat::UnsignedRoundingMode::HalfZero; - case NumberFormat::RoundingMode::HalfExpand: - return NumberFormat::UnsignedRoundingMode::HalfInfinity; - case NumberFormat::RoundingMode::HalfTrunc: - return NumberFormat::UnsignedRoundingMode::HalfZero; - case NumberFormat::RoundingMode::HalfEven: - return NumberFormat::UnsignedRoundingMode::HalfEven; - default: - VERIFY_NOT_REACHED(); - }; -} - -// 15.5.18 ApplyUnsignedRoundingMode ( x, r1, r2, unsignedRoundingMode ), https://tc39.es/ecma402/#sec-applyunsignedroundingmode -RoundingDecision apply_unsigned_rounding_mode(MathematicalValue const& x, MathematicalValue const& r1, MathematicalValue const& r2, NumberFormat::UnsignedRoundingMode unsigned_rounding_mode) -{ - // 1. If x is equal to r1, return r1. - if (x.is_equal_to(r1)) - return RoundingDecision::LowerValue; - - // FIXME: We skip this assertion due floating point inaccuracies. For example, entering "1.2345" - // in the JS REPL results in "1.234499999999999", and may cause this assertion to fail. - // - // This should be resolved when the "Intl mathematical value" is implemented to support - // arbitrarily precise decimals. - // https://tc39.es/ecma402/#intl-mathematical-value - // 2. Assert: r1 < x < r2. - - // 3. Assert: unsignedRoundingMode is not undefined. - - // 4. If unsignedRoundingMode is zero, return r1. - if (unsigned_rounding_mode == NumberFormat::UnsignedRoundingMode::Zero) - return RoundingDecision::LowerValue; - - // 5. If unsignedRoundingMode is infinity, return r2. - if (unsigned_rounding_mode == NumberFormat::UnsignedRoundingMode::Infinity) - return RoundingDecision::HigherValue; - - // 6. Let d1 be x – r1. - auto d1 = x.minus(r1); - - // 7. Let d2 be r2 – x. - auto d2 = r2.minus(x); - - // 8. If d1 < d2, return r1. - if (d1.is_less_than(d2)) - return RoundingDecision::LowerValue; - - // 9. If d2 < d1, return r2. - if (d2.is_less_than(d1)) - return RoundingDecision::HigherValue; - - // 10. Assert: d1 is equal to d2. - VERIFY(d1.is_equal_to(d2)); - - // 11. If unsignedRoundingMode is half-zero, return r1. - if (unsigned_rounding_mode == NumberFormat::UnsignedRoundingMode::HalfZero) - return RoundingDecision::LowerValue; - - // 12. If unsignedRoundingMode is half-infinity, return r2. - if (unsigned_rounding_mode == NumberFormat::UnsignedRoundingMode::HalfInfinity) - return RoundingDecision::HigherValue; - - // 13. Assert: unsignedRoundingMode is half-even. - VERIFY(unsigned_rounding_mode == NumberFormat::UnsignedRoundingMode::HalfEven); - - // 14. Let cardinality be (r1 / (r2 – r1)) modulo 2. - auto cardinality = r1.divided_by(r2.minus(r1)); - - // 15. If cardinality is 0, return r1. - if (cardinality.modulo_is_zero(2)) - return RoundingDecision::LowerValue; - - // 16. Return r2. - return RoundingDecision::HigherValue; -} - // 15.5.19 PartitionNumberRangePattern ( numberFormat, x, y ), https://tc39.es/ecma402/#sec-partitionnumberrangepattern ThrowCompletionOr> partition_number_range_pattern(VM& vm, NumberFormat& number_format, MathematicalValue start, MathematicalValue end) { @@ -1803,16 +241,16 @@ ThrowCompletionOr> partition_number_range_pat Vector result; // 3. Let xResult be ? PartitionNumberPattern(numberFormat, x). - auto raw_start_result = partition_number_pattern(vm, number_format, move(start)); + auto raw_start_result = partition_number_pattern(number_format, start); auto start_result = PatternPartitionWithSource::create_from_parent_list(move(raw_start_result)); // 4. Let yResult be ? PartitionNumberPattern(numberFormat, y). - auto raw_end_result = partition_number_pattern(vm, number_format, move(end)); + auto raw_end_result = partition_number_pattern(number_format, end); auto end_result = PatternPartitionWithSource::create_from_parent_list(move(raw_end_result)); // 5. If ! FormatNumeric(numberFormat, x) is equal to ! FormatNumeric(numberFormat, y), then - auto formatted_start = format_numeric(vm, number_format, start); - auto formatted_end = format_numeric(vm, number_format, end); + auto formatted_start = format_numeric(number_format, start); + auto formatted_end = format_numeric(number_format, end); if (formatted_start == formatted_end) { // a. Let appxResult be ? FormatApproximately(numberFormat, xResult). diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h index 55dc2bc6934..ca05c0af92e 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormat.h @@ -22,46 +22,11 @@ class NumberFormatBase : public Object { JS_DECLARE_ALLOCATOR(NumberFormatBase); public: - enum class RoundingType { - Invalid, - SignificantDigits, - FractionDigits, - MorePrecision, - LessPrecision, - }; - enum class ComputedRoundingPriority { - Invalid, Auto, MorePrecision, LessPrecision, - }; - - enum class RoundingMode { Invalid, - Ceil, - Expand, - Floor, - HalfCeil, - HalfEven, - HalfExpand, - HalfFloor, - HalfTrunc, - Trunc, - }; - - enum class UnsignedRoundingMode { - HalfEven, - HalfInfinity, - HalfZero, - Infinity, - Zero, - }; - - enum class TrailingZeroDisplay { - Invalid, - Auto, - StripIfInteger, }; virtual ~NumberFormatBase() override = default; @@ -91,24 +56,29 @@ public: int max_significant_digits() const { return *m_max_significant_digits; } void set_max_significant_digits(int max_significant_digits) { m_max_significant_digits = max_significant_digits; } - RoundingType rounding_type() const { return m_rounding_type; } - StringView rounding_type_string() const; - void set_rounding_type(RoundingType rounding_type) { m_rounding_type = rounding_type; } + ::Locale::RoundingType rounding_type() const { return m_rounding_type; } + StringView rounding_type_string() const { return ::Locale::rounding_type_to_string(m_rounding_type); } + void set_rounding_type(::Locale::RoundingType rounding_type) { m_rounding_type = rounding_type; } ComputedRoundingPriority computed_rounding_priority() const { return m_computed_rounding_priority; } StringView computed_rounding_priority_string() const; void set_computed_rounding_priority(ComputedRoundingPriority computed_rounding_priority) { m_computed_rounding_priority = computed_rounding_priority; } - RoundingMode rounding_mode() const { return m_rounding_mode; } - StringView rounding_mode_string() const; - void set_rounding_mode(StringView rounding_mode); + ::Locale::RoundingMode rounding_mode() const { return m_rounding_mode; } + StringView rounding_mode_string() const { return ::Locale::rounding_mode_to_string(m_rounding_mode); } + void set_rounding_mode(StringView rounding_mode) { m_rounding_mode = ::Locale::rounding_mode_from_string(rounding_mode); } int rounding_increment() const { return m_rounding_increment; } void set_rounding_increment(int rounding_increment) { m_rounding_increment = rounding_increment; } - TrailingZeroDisplay trailing_zero_display() const { return m_trailing_zero_display; } - StringView trailing_zero_display_string() const; - void set_trailing_zero_display(StringView trailing_zero_display); + ::Locale::TrailingZeroDisplay trailing_zero_display() const { return m_trailing_zero_display; } + StringView trailing_zero_display_string() const { return ::Locale::trailing_zero_display_to_string(m_trailing_zero_display); } + void set_trailing_zero_display(StringView trailing_zero_display) { m_trailing_zero_display = ::Locale::trailing_zero_display_from_string(trailing_zero_display); } + + ::Locale::RoundingOptions rounding_options() const; + + ::Locale::NumberFormat const& formatter() const { return *m_formatter; } + void set_formatter(NonnullOwnPtr<::Locale::NumberFormat> formatter) { m_formatter = move(formatter); } protected: explicit NumberFormatBase(Object& prototype); @@ -121,11 +91,14 @@ private: Optional m_max_fraction_digits {}; // [[MaximumFractionDigits]] Optional m_min_significant_digits {}; // [[MinimumSignificantDigits]] Optional m_max_significant_digits {}; // [[MaximumSignificantDigits]] - RoundingType m_rounding_type { RoundingType::Invalid }; // [[RoundingType]] + ::Locale::RoundingType m_rounding_type; // [[RoundingType]] ComputedRoundingPriority m_computed_rounding_priority { ComputedRoundingPriority::Invalid }; // [[ComputedRoundingPriority]] - RoundingMode m_rounding_mode { RoundingMode::Invalid }; // [[RoundingMode]] + ::Locale::RoundingMode m_rounding_mode; // [[RoundingMode]] int m_rounding_increment { 1 }; // [[RoundingIncrement]] - TrailingZeroDisplay m_trailing_zero_display { TrailingZeroDisplay::Invalid }; // [[TrailingZeroDisplay]] + ::Locale::TrailingZeroDisplay m_trailing_zero_display; // [[TrailingZeroDisplay]] + + // Non-standard. Stores the ICU number formatter for the Intl object's formatting options. + OwnPtr<::Locale::NumberFormat> m_formatter; }; class NumberFormat final : public NumberFormatBase { @@ -133,56 +106,6 @@ class NumberFormat final : public NumberFormatBase { JS_DECLARE_ALLOCATOR(NumberFormat); public: - enum class Style { - Invalid, - Decimal, - Percent, - Currency, - Unit, - }; - - enum class CurrencyDisplay { - Code, - Symbol, - NarrowSymbol, - Name, - }; - - enum class CurrencySign { - Standard, - Accounting, - }; - - enum class Notation { - Invalid, - Standard, - Scientific, - Engineering, - Compact, - }; - - enum class CompactDisplay { - Short, - Long, - }; - - enum class SignDisplay { - Invalid, - Auto, - Never, - Always, - ExceptZero, - Negative, - }; - - enum class UseGrouping { - Invalid, - Always, - Auto, - Min2, - False, - }; - static constexpr auto relevant_extension_keys() { // 15.2.3 Internal slots, https://tc39.es/ecma402/#sec-intl.numberformat-internal-slots @@ -195,24 +118,23 @@ public: String const& numbering_system() const { return m_numbering_system; } void set_numbering_system(String numbering_system) { m_numbering_system = move(numbering_system); } - Style style() const { return m_style; } - StringView style_string() const; - void set_style(StringView style); + ::Locale::NumberFormatStyle style() const { return m_style; } + StringView style_string() const { return ::Locale::number_format_style_to_string(m_style); } + void set_style(StringView style) { m_style = ::Locale::number_format_style_from_string(style); } bool has_currency() const { return m_currency.has_value(); } String const& currency() const { return m_currency.value(); } void set_currency(String currency) { m_currency = move(currency); } bool has_currency_display() const { return m_currency_display.has_value(); } - CurrencyDisplay currency_display() const { return *m_currency_display; } - StringView currency_display_string() const; - void set_currency_display(StringView currency_display); - StringView resolve_currency_display(); + ::Locale::CurrencyDisplay currency_display() const { return *m_currency_display; } + StringView currency_display_string() const { return ::Locale::currency_display_to_string(*m_currency_display); } + void set_currency_display(StringView currency_display) { m_currency_display = ::Locale::currency_display_from_string(currency_display); } bool has_currency_sign() const { return m_currency_sign.has_value(); } - CurrencySign currency_sign() const { return *m_currency_sign; } - StringView currency_sign_string() const; - void set_currency_sign(StringView set_currency_sign); + ::Locale::CurrencySign currency_sign() const { return *m_currency_sign; } + StringView currency_sign_string() const { return ::Locale::currency_sign_to_string(*m_currency_sign); } + void set_currency_sign(StringView currency_sign) { m_currency_sign = ::Locale::currency_sign_from_string(currency_sign); } bool has_unit() const { return m_unit.has_value(); } String const& unit() const { return m_unit.value(); } @@ -223,87 +145,55 @@ public: StringView unit_display_string() const { return ::Locale::style_to_string(*m_unit_display); } void set_unit_display(StringView unit_display) { m_unit_display = ::Locale::style_from_string(unit_display); } - UseGrouping use_grouping() const { return m_use_grouping; } + ::Locale::Grouping use_grouping() const { return m_use_grouping; } Value use_grouping_to_value(VM&) const; void set_use_grouping(StringOrBoolean const& use_grouping); - Notation notation() const { return m_notation; } - StringView notation_string() const; - void set_notation(StringView notation); + ::Locale::Notation notation() const { return m_notation; } + StringView notation_string() const { return ::Locale::notation_to_string(m_notation); } + void set_notation(StringView notation) { m_notation = ::Locale::notation_from_string(notation); } bool has_compact_display() const { return m_compact_display.has_value(); } - CompactDisplay compact_display() const { return *m_compact_display; } - StringView compact_display_string() const; - void set_compact_display(StringView compact_display); + ::Locale::CompactDisplay compact_display() const { return *m_compact_display; } + StringView compact_display_string() const { return ::Locale::compact_display_to_string(*m_compact_display); } + void set_compact_display(StringView compact_display) { m_compact_display = ::Locale::compact_display_from_string(compact_display); } - SignDisplay sign_display() const { return m_sign_display; } - StringView sign_display_string() const; - void set_sign_display(StringView sign_display); + ::Locale::SignDisplay sign_display() const { return m_sign_display; } + StringView sign_display_string() const { return ::Locale::sign_display_to_string(m_sign_display); } + void set_sign_display(StringView sign_display) { m_sign_display = ::Locale::sign_display_from_string(sign_display); } NativeFunction* bound_format() const { return m_bound_format; } void set_bound_format(NativeFunction* bound_format) { m_bound_format = bound_format; } - bool has_compact_format() const { return m_compact_format.has_value(); } - void set_compact_format(::Locale::NumberFormat compact_format) { m_compact_format = compact_format; } - ::Locale::NumberFormat compact_format() const { return *m_compact_format; } + ::Locale::DisplayOptions display_options() const; private: explicit NumberFormat(Object& prototype); virtual void visit_edges(Visitor&) override; - String m_locale; // [[Locale]] - String m_data_locale; // [[DataLocale]] - String m_numbering_system; // [[NumberingSystem]] - Style m_style { Style::Invalid }; // [[Style]] - Optional m_currency {}; // [[Currency]] - Optional m_currency_display {}; // [[CurrencyDisplay]] - Optional m_currency_sign {}; // [[CurrencySign]] - Optional m_unit {}; // [[Unit]] - Optional<::Locale::Style> m_unit_display {}; // [[UnitDisplay]] - UseGrouping m_use_grouping { UseGrouping::False }; // [[UseGrouping]] - Notation m_notation { Notation::Invalid }; // [[Notation]] - Optional m_compact_display {}; // [[CompactDisplay]] - SignDisplay m_sign_display { SignDisplay::Invalid }; // [[SignDisplay]] - GCPtr m_bound_format; // [[BoundFormat]] - - // Non-standard. Stores the resolved currency display string based on [[Locale]], [[Currency]], and [[CurrencyDisplay]]. - Optional m_resolved_currency_display; - - // Non-standard. Stores the resolved compact number format based on [[Locale]], [[Notation], [[Style]], and [[CompactDisplay]]. - Optional<::Locale::NumberFormat> m_compact_format; -}; - -struct FormatResult { - String formatted_string; // [[FormattedString]] - MathematicalValue rounded_number { 0.0 }; // [[RoundedNumber]] -}; - -struct RawFormatResult : public FormatResult { - int digits { 0 }; // [[IntegerDigitsCount]] - int rounding_magnitude { 0 }; // [[RoundingMagnitude]] -}; - -enum class RoundingDecision { - LowerValue, - HigherValue, + String m_locale; // [[Locale]] + String m_data_locale; // [[DataLocale]] + String m_numbering_system; // [[NumberingSystem]] + ::Locale::NumberFormatStyle m_style; // [[Style]] + Optional m_currency; // [[Currency]] + Optional<::Locale::CurrencyDisplay> m_currency_display; // [[CurrencyDisplay]] + Optional<::Locale::CurrencySign> m_currency_sign; // [[CurrencySign]] + Optional m_unit; // [[Unit]] + Optional<::Locale::Style> m_unit_display; // [[UnitDisplay]] + ::Locale::Grouping m_use_grouping { ::Locale::Grouping::False }; // [[UseGrouping]] + ::Locale::Notation m_notation; // [[Notation]] + Optional<::Locale::CompactDisplay> m_compact_display; // [[CompactDisplay]] + ::Locale::SignDisplay m_sign_display; // [[SignDisplay]] + GCPtr m_bound_format; // [[BoundFormat]] }; int currency_digits(StringView currency); -FormatResult format_numeric_to_string(NumberFormatBase const& intl_object, MathematicalValue number); -Vector partition_number_pattern(VM&, NumberFormat&, MathematicalValue number); -Vector partition_notation_sub_pattern(NumberFormat&, MathematicalValue const& number, String formatted_string, int exponent); -String format_numeric(VM&, NumberFormat&, MathematicalValue number); -NonnullGCPtr format_numeric_to_parts(VM&, NumberFormat&, MathematicalValue number); -RawFormatResult to_raw_precision(MathematicalValue const& number, int min_precision, int max_precision, NumberFormat::UnsignedRoundingMode unsigned_rounding_mode); -RawFormatResult to_raw_fixed(MathematicalValue const& number, int min_fraction, int max_fraction, int rounding_increment, NumberFormat::UnsignedRoundingMode unsigned_rounding_mode); -Optional> get_number_format_pattern(VM&, NumberFormat&, MathematicalValue const& number, ::Locale::NumberFormat& found_pattern); -Optional get_notation_sub_pattern(NumberFormat&, int exponent); -int compute_exponent(NumberFormat&, MathematicalValue number); -int compute_exponent_for_magnitude(NumberFormat&, int magnitude); +String format_numeric_to_string(NumberFormatBase const& intl_object, MathematicalValue const& number); +Vector<::Locale::NumberFormat::Partition> partition_number_pattern(NumberFormat const&, MathematicalValue const& number); +String format_numeric(NumberFormat const&, MathematicalValue const& number); +NonnullGCPtr format_numeric_to_parts(VM&, NumberFormat const&, MathematicalValue const& number); ThrowCompletionOr to_intl_mathematical_value(VM&, Value value); -NumberFormat::UnsignedRoundingMode get_unsigned_rounding_mode(NumberFormat::RoundingMode, bool is_negative); -RoundingDecision apply_unsigned_rounding_mode(MathematicalValue const& x, MathematicalValue const& r1, MathematicalValue const& r2, NumberFormat::UnsignedRoundingMode unsigned_rounding_mode); ThrowCompletionOr> partition_number_range_pattern(VM&, NumberFormat&, MathematicalValue start, MathematicalValue end); Vector format_approximately(NumberFormat&, Vector result); Vector collapse_number_range(Vector result); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatConstructor.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatConstructor.cpp index 0ef735c9a16..12ed3f37737 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatConstructor.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -135,7 +135,7 @@ ThrowCompletionOr> initialize_number_format(VM& vm, N int default_max_fraction_digits = 0; // 16. If style is "currency", then - if (style == NumberFormat::Style::Currency) { + if (style == ::Locale::NumberFormatStyle::Currency) { // a. Let currency be numberFormat.[[Currency]]. auto const& currency = number_format.currency(); @@ -157,7 +157,7 @@ ThrowCompletionOr> initialize_number_format(VM& vm, N // i. Let mxfdDefault be 0. // c. Else, // i. Let mxfdDefault be 3. - default_max_fraction_digits = style == NumberFormat::Style::Percent ? 0 : 3; + default_max_fraction_digits = style == ::Locale::NumberFormatStyle::Percent ? 0 : 3; } // 18. Let notation be ? GetOption(options, "notation", string, « "standard", "scientific", "engineering", "compact" », "standard"). @@ -176,7 +176,7 @@ ThrowCompletionOr> initialize_number_format(VM& vm, N auto default_use_grouping = "auto"sv; // 23. If notation is "compact", then - if (number_format.notation() == NumberFormat::Notation::Compact) { + if (number_format.notation() == ::Locale::Notation::Compact) { // a. Set numberFormat.[[CompactDisplay]] to compactDisplay. number_format.set_compact_display(compact_display.as_string().utf8_string_view()); @@ -209,12 +209,20 @@ ThrowCompletionOr> initialize_number_format(VM& vm, N // 30. Set numberFormat.[[SignDisplay]] to signDisplay. number_format.set_sign_display(sign_display.as_string().utf8_string_view()); + // Non-standard, create an ICU number formatter for this Intl object. + auto formatter = ::Locale::NumberFormat::create( + number_format.locale(), + number_format.numbering_system(), + number_format.display_options(), + number_format.rounding_options()); + number_format.set_formatter(move(formatter)); + // 31. Return numberFormat. return number_format; } // 15.1.3 SetNumberFormatDigitOptions ( intlObj, options, mnfdDefault, mxfdDefault, notation ), https://tc39.es/ecma402/#sec-setnfdigitoptions -ThrowCompletionOr set_number_format_digit_options(VM& vm, NumberFormatBase& intl_object, Object const& options, int default_min_fraction_digits, int default_max_fraction_digits, NumberFormat::Notation notation) +ThrowCompletionOr set_number_format_digit_options(VM& vm, NumberFormatBase& intl_object, Object const& options, int default_min_fraction_digits, int default_max_fraction_digits, ::Locale::Notation notation) { // 1. Let mnid be ? GetNumberOption(options, "minimumIntegerDigits,", 1, 21, 1). auto min_integer_digits = TRY(get_number_option(vm, options, vm.names.minimumIntegerDigits, 1, 21, 1)); @@ -292,7 +300,7 @@ ThrowCompletionOr set_number_format_digit_options(VM& vm, NumberFormatBase need_significant_digits = has_significant_digits; // b. If hasSd is true, or hasFd is false and notation is "compact", then - if (has_significant_digits || (!has_fraction_digits && notation == NumberFormat::Notation::Compact)) { + if (has_significant_digits || (!has_fraction_digits && notation == ::Locale::Notation::Compact)) { // i. Set needFd to false. need_fraction_digits = false; } @@ -371,7 +379,7 @@ ThrowCompletionOr set_number_format_digit_options(VM& vm, NumberFormatBase intl_object.set_max_significant_digits(2); // e. Set intlObj.[[RoundingType]] to morePrecision. - intl_object.set_rounding_type(NumberFormatBase::RoundingType::MorePrecision); + intl_object.set_rounding_type(::Locale::RoundingType::MorePrecision); // f. Set intlObj.[[ComputedRoundingPriority]] to "morePrecision". intl_object.set_computed_rounding_priority(NumberFormatBase::ComputedRoundingPriority::MorePrecision); @@ -379,7 +387,7 @@ ThrowCompletionOr set_number_format_digit_options(VM& vm, NumberFormatBase // 27. Else if roundingPriority is "morePrecision", then else if (rounding_priority == "morePrecision"sv) { // a. Set intlObj.[[RoundingType]] to morePrecision. - intl_object.set_rounding_type(NumberFormatBase::RoundingType::MorePrecision); + intl_object.set_rounding_type(::Locale::RoundingType::MorePrecision); // b. Set intlObj.[[ComputedRoundingPriority]] to "morePrecision". intl_object.set_computed_rounding_priority(NumberFormatBase::ComputedRoundingPriority::MorePrecision); @@ -387,7 +395,7 @@ ThrowCompletionOr set_number_format_digit_options(VM& vm, NumberFormatBase // 28. Else if roundingPriority is "lessPrecision", then else if (rounding_priority == "lessPrecision"sv) { // a. Set intlObj.[[RoundingType]] to lessPrecision. - intl_object.set_rounding_type(NumberFormatBase::RoundingType::LessPrecision); + intl_object.set_rounding_type(::Locale::RoundingType::LessPrecision); // b. Set intlObj.[[ComputedRoundingPriority]] to "lessPrecision". intl_object.set_computed_rounding_priority(NumberFormatBase::ComputedRoundingPriority::LessPrecision); @@ -395,7 +403,7 @@ ThrowCompletionOr set_number_format_digit_options(VM& vm, NumberFormatBase // 29. Else if hasSd is true, then else if (has_significant_digits) { // a. Set intlObj.[[RoundingType]] to significantDigits. - intl_object.set_rounding_type(NumberFormatBase::RoundingType::SignificantDigits); + intl_object.set_rounding_type(::Locale::RoundingType::SignificantDigits); // b. Set intlObj.[[ComputedRoundingPriority]] to "auto". intl_object.set_computed_rounding_priority(NumberFormatBase::ComputedRoundingPriority::Auto); @@ -403,7 +411,7 @@ ThrowCompletionOr set_number_format_digit_options(VM& vm, NumberFormatBase // 30. Else, else { // a. Set intlObj.[[RoundingType]] to fractionDigits. - intl_object.set_rounding_type(NumberFormatBase::RoundingType::FractionDigits); + intl_object.set_rounding_type(::Locale::RoundingType::FractionDigits); // b. Set intlObj.[[ComputedRoundingPriority]] to "auto". intl_object.set_computed_rounding_priority(NumberFormatBase::ComputedRoundingPriority::Auto); @@ -412,7 +420,7 @@ ThrowCompletionOr set_number_format_digit_options(VM& vm, NumberFormatBase // 31. If roundingIncrement is not 1, then if (rounding_increment != 1) { // a. If intlObj.[[RoundingType]] is not fractionDigits, throw a TypeError exception. - if (intl_object.rounding_type() != NumberFormatBase::RoundingType::FractionDigits) + if (intl_object.rounding_type() != ::Locale::RoundingType::FractionDigits) return vm.throw_completion(ErrorType::IntlInvalidRoundingIncrementForRoundingType, *rounding_increment, intl_object.rounding_type_string()); // b. If intlObj.[[MaximumFractionDigits]] is not equal to intlObj.[[MinimumFractionDigits]], throw a RangeError exception. @@ -441,7 +449,7 @@ ThrowCompletionOr set_number_format_unit_options(VM& vm, NumberFormat& int // 6. If currency is undefined, then if (currency.is_undefined()) { // a. If style is "currency", throw a TypeError exception. - if (intl_object.style() == NumberFormat::Style::Currency) + if (intl_object.style() == ::Locale::NumberFormatStyle::Currency) return vm.throw_completion(ErrorType::IntlOptionUndefined, "currency"sv, "style"sv, style); } // 7. Else, @@ -461,7 +469,7 @@ ThrowCompletionOr set_number_format_unit_options(VM& vm, NumberFormat& int // 11. If unit is undefined, then if (unit.is_undefined()) { // a. If style is "unit", throw a TypeError exception. - if (intl_object.style() == NumberFormat::Style::Unit) + if (intl_object.style() == ::Locale::NumberFormatStyle::Unit) return vm.throw_completion(ErrorType::IntlOptionUndefined, "unit"sv, "style"sv, style); } // 12. Else, @@ -473,7 +481,7 @@ ThrowCompletionOr set_number_format_unit_options(VM& vm, NumberFormat& int auto unit_display = TRY(get_option(vm, options, vm.names.unitDisplay, OptionType::String, { "short"sv, "narrow"sv, "long"sv }, "short"sv)); // 14. If style is "currency", then - if (intl_object.style() == NumberFormat::Style::Currency) { + if (intl_object.style() == ::Locale::NumberFormatStyle::Currency) { // a. Set intlObj.[[Currency]] to the ASCII-uppercase of currency. intl_object.set_currency(MUST(currency.as_string().utf8_string().to_uppercase())); @@ -485,7 +493,7 @@ ThrowCompletionOr set_number_format_unit_options(VM& vm, NumberFormat& int } // 15. If style is "unit", then - if (intl_object.style() == NumberFormat::Style::Unit) { + if (intl_object.style() == ::Locale::NumberFormatStyle::Unit) { // a. Set intlObj.[[Unit]] to unit. intl_object.set_unit(unit.as_string().utf8_string()); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatConstructor.h b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatConstructor.h index a3cd15b8f3e..32e9aaab7db 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatConstructor.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatConstructor.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2022, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -31,7 +31,7 @@ private: }; ThrowCompletionOr> initialize_number_format(VM&, NumberFormat&, Value locales_value, Value options_value); -ThrowCompletionOr set_number_format_digit_options(VM&, NumberFormatBase& intl_object, Object const& options, int default_min_fraction_digits, int default_max_fraction_digits, NumberFormat::Notation notation); +ThrowCompletionOr set_number_format_digit_options(VM&, NumberFormatBase& intl_object, Object const& options, int default_min_fraction_digits, int default_max_fraction_digits, ::Locale::Notation notation); ThrowCompletionOr set_number_format_unit_options(VM&, NumberFormat& intl_object, Object const& options); } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatFunction.cpp b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatFunction.cpp index 80e64d48f7c..737a50c0b6a 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatFunction.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/NumberFormatFunction.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -46,7 +46,7 @@ ThrowCompletionOr NumberFormatFunction::call() auto mathematical_value = TRY(to_intl_mathematical_value(vm, value)); // 5. Return ? FormatNumeric(nf, x). - auto formatted = format_numeric(vm, m_number_format, move(mathematical_value)); + auto formatted = format_numeric(m_number_format, move(mathematical_value)); return PrimitiveString::create(vm, move(formatted)); } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.cpp b/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.cpp index b67a8b2d0f5..b9077b6fa91 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -95,12 +95,6 @@ PluralRules::PluralRules(Object& prototype) // 16.5.3 ResolvePlural ( pluralRules, n ), https://tc39.es/ecma402/#sec-resolveplural ResolvedPlurality resolve_plural(PluralRules const& plural_rules, Value number) -{ - return resolve_plural(plural_rules, plural_rules.type(), number); -} - -// Non-standard overload of ResolvePlural to allow using the AO without an Intl.PluralRules object. -ResolvedPlurality resolve_plural(NumberFormatBase const& number_format, ::Locale::PluralForm type, Value number) { // 1. Assert: Type(pluralRules) is Object. // 2. Assert: pluralRules has an [[InitializedPluralRules]] internal slot. @@ -113,15 +107,16 @@ ResolvedPlurality resolve_plural(NumberFormatBase const& number_format, ::Locale } // 5. Let locale be pluralRules.[[Locale]]. - auto const& locale = number_format.locale(); + auto const& locale = plural_rules.locale(); // 6. Let type be pluralRules.[[Type]]. + auto type = plural_rules.type(); // 7. Let res be ! FormatNumericToString(pluralRules, n). - auto result = format_numeric_to_string(number_format, number); + auto result = format_numeric_to_string(plural_rules, number); // 8. Let s be res.[[FormattedString]]. - auto string = move(result.formatted_string); + auto string = move(result); // 9. Let operands be ! GetOperands(s). auto operands = get_operands(string); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.h b/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.h index e9ec20e5909..d5a9058ac04 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/PluralRules.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -40,7 +40,6 @@ struct ResolvedPlurality { ::Locale::PluralOperands get_operands(StringView string); ::Locale::PluralCategory plural_rule_select(StringView locale, ::Locale::PluralForm type, Value number, ::Locale::PluralOperands operands); ResolvedPlurality resolve_plural(PluralRules const&, Value number); -ResolvedPlurality resolve_plural(NumberFormatBase const& number_format, ::Locale::PluralForm type, Value number); ::Locale::PluralCategory plural_rule_select_range(StringView locale, ::Locale::PluralForm, ::Locale::PluralCategory start, ::Locale::PluralCategory end); ThrowCompletionOr<::Locale::PluralCategory> resolve_plural_range(VM&, PluralRules const&, Value start, Value end); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesConstructor.cpp b/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesConstructor.cpp index d9d1e8bb19c..1f4c97290ef 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/PluralRulesConstructor.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -99,7 +99,7 @@ ThrowCompletionOr> initialize_plural_rules(VM& vm, Plu plural_rules.set_type(type.as_string().utf8_string_view()); // 8. Perform ? SetNumberFormatDigitOptions(pluralRules, options, +0𝔽, 3𝔽, "standard"). - TRY(set_number_format_digit_options(vm, plural_rules, *options, 0, 3, NumberFormat::Notation::Standard)); + TRY(set_number_format_digit_options(vm, plural_rules, *options, 0, 3, ::Locale::Notation::Standard)); // 9. Let localeData be %PluralRules%.[[LocaleData]]. // 10. Let r be ResolveLocale(%PluralRules%.[[AvailableLocales]], requestedLocales, opt, %PluralRules%.[[RelevantExtensionKeys]], localeData). @@ -111,6 +111,14 @@ ThrowCompletionOr> initialize_plural_rules(VM& vm, Plu // Non-standard, the data locale is used by our NumberFormat implementation. plural_rules.set_data_locale(move(result.data_locale)); + // Non-standard, create an ICU number formatter for this Intl object. + auto formatter = ::Locale::NumberFormat::create( + plural_rules.locale(), + {}, + {}, + plural_rules.rounding_options()); + plural_rules.set_formatter(move(formatter)); + // 12. Return pluralRules. return plural_rules; } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.cpp index 9149749bf41..1f752c04ac4 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -176,7 +176,7 @@ ThrowCompletionOr> partition_relative_time_patt auto patterns = find_patterns_for_tense_or_number(tense); // 20. Let fv be ! PartitionNumberPattern(relativeTimeFormat.[[NumberFormat]], value). - auto value_partitions = partition_number_pattern(vm, relative_time_format.number_format(), Value(value)); + auto value_partitions = partition_number_pattern(relative_time_format.number_format(), Value(value)); // 21. Let pr be ! ResolvePlural(relativeTimeFormat.[[PluralRules]], value).[[PluralCategory]]. auto plurality = resolve_plural(relative_time_format.plural_rules(), Value(value)); @@ -191,7 +191,7 @@ ThrowCompletionOr> partition_relative_time_patt } // 17.5.3 MakePartsList ( pattern, unit, parts ), https://tc39.es/ecma402/#sec-makepartslist -Vector make_parts_list(StringView pattern, StringView unit, Vector parts) +Vector make_parts_list(StringView pattern, StringView unit, Vector<::Locale::NumberFormat::Partition> parts) { // 1. Let patternParts be PartitionPattern(pattern). auto pattern_parts = partition_pattern(pattern); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.h b/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.h index 568c9e43d31..be07b0e05f5 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -13,6 +13,7 @@ #include #include #include +#include #include namespace JS::Intl { @@ -85,7 +86,7 @@ struct PatternPartitionWithUnit : public PatternPartition { ThrowCompletionOr<::Locale::TimeUnit> singular_relative_time_unit(VM&, StringView unit); ThrowCompletionOr> partition_relative_time_pattern(VM&, RelativeTimeFormat&, double value, StringView unit); -Vector make_parts_list(StringView pattern, StringView unit, Vector parts); +Vector make_parts_list(StringView pattern, StringView unit, Vector<::Locale::NumberFormat::Partition> parts); ThrowCompletionOr format_relative_time(VM&, RelativeTimeFormat&, double value, StringView unit); ThrowCompletionOr> format_relative_time_to_parts(VM&, RelativeTimeFormat&, double value, StringView unit); diff --git a/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp b/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp index dd06de327b1..34e22fcee1b 100644 --- a/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp +++ b/Userland/Libraries/LibJS/Runtime/NumberPrototype.cpp @@ -275,7 +275,7 @@ JS_DEFINE_NATIVE_FUNCTION(NumberPrototype::to_locale_string) auto* number_format = static_cast(TRY(construct(vm, realm.intrinsics().intl_number_format_constructor(), locales, options)).ptr()); // 3. Return ? FormatNumeric(numberFormat, x). - auto formatted = Intl::format_numeric(vm, *number_format, number_value); + auto formatted = Intl::format_numeric(*number_format, number_value); return PrimitiveString::create(vm, move(formatted)); } 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 a21b2804c9b..c96fb46fc11 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 @@ -1065,13 +1065,13 @@ describe("style=percent", () => { expect(enFullwide.format(0.1234)).toBe("12%"); const ar = new Intl.NumberFormat("ar", { style: "percent", notation: "compact" }); - expect(ar.format(0.01)).toBe("\u0661\u066a\u061c"); - expect(ar.format(0.012)).toBe("\u0661\u066b\u0662\u066a\u061c"); - expect(ar.format(0.0123)).toBe("\u0661\u066b\u0662\u066a\u061c"); - expect(ar.format(0.0129)).toBe("\u0661\u066b\u0663\u066a\u061c"); - expect(ar.format(0.12)).toBe("\u0661\u0662\u066a\u061c"); - expect(ar.format(0.123)).toBe("\u0661\u0662\u066a\u061c"); - expect(ar.format(0.1234)).toBe("\u0661\u0662\u066a\u061c"); + expect(ar.format(0.01)).toBe("\u0661\u066a"); + expect(ar.format(0.012)).toBe("\u0661\u066b\u0662\u066a"); + expect(ar.format(0.0123)).toBe("\u0661\u066b\u0662\u066a"); + expect(ar.format(0.0129)).toBe("\u0661\u066b\u0663\u066a"); + expect(ar.format(0.12)).toBe("\u0661\u0662\u066a"); + expect(ar.format(0.123)).toBe("\u0661\u0662\u066a"); + expect(ar.format(0.1234)).toBe("\u0661\u0662\u066a"); }); test("signDisplay=never", () => { @@ -1322,14 +1322,14 @@ describe("style=currency", () => { compactDisplay: "long", }); expect(ar.format(1)).toBe("‏١ US$"); - expect(ar.format(1200)).toBe("‏١٫٢ ألف US$"); - expect(ar.format(1290)).toBe("‏١٫٣ ألف US$"); - expect(ar.format(12000)).toBe("‏١٢ ألف US$"); - expect(ar.format(12900)).toBe("‏١٣ ألف US$"); - expect(ar.format(1200000)).toBe("‏١٫٢ مليون US$"); - expect(ar.format(1290000)).toBe("‏١٫٣ مليون US$"); - expect(ar.format(12000000)).toBe("‏١٢ مليون US$"); - expect(ar.format(12900000)).toBe("‏١٣ مليون US$"); + expect(ar.format(1200)).toBe("١٫٢ ألف US$"); + expect(ar.format(1290)).toBe("١٫٣ ألف US$"); + expect(ar.format(12000)).toBe("١٢ ألف US$"); + expect(ar.format(12900)).toBe("١٣ ألف US$"); + expect(ar.format(1200000)).toBe("١٫٢ مليون US$"); + expect(ar.format(1290000)).toBe("١٫٣ مليون US$"); + expect(ar.format(12000000)).toBe("١٢ مليون US$"); + expect(ar.format(12900000)).toBe("١٣ مليون US$"); const ja = new Intl.NumberFormat("ja", { style: "currency", @@ -1392,14 +1392,14 @@ describe("style=currency", () => { compactDisplay: "short", }); expect(ar.format(1)).toBe("‏١ US$"); - expect(ar.format(1200)).toBe("‏١٫٢ ألف US$"); - expect(ar.format(1290)).toBe("‏١٫٣ ألف US$"); - expect(ar.format(12000)).toBe("‏١٢ ألف US$"); - expect(ar.format(12900)).toBe("‏١٣ ألف US$"); - expect(ar.format(1200000)).toBe("‏١٫٢ مليون US$"); - expect(ar.format(1290000)).toBe("‏١٫٣ مليون US$"); - expect(ar.format(12000000)).toBe("‏١٢ مليون US$"); - expect(ar.format(12900000)).toBe("‏١٣ مليون US$"); + expect(ar.format(1200)).toBe("١٫٢ ألف US$"); + expect(ar.format(1290)).toBe("١٫٣ ألف US$"); + expect(ar.format(12000)).toBe("١٢ ألف US$"); + expect(ar.format(12900)).toBe("١٣ ألف US$"); + expect(ar.format(1200000)).toBe("١٫٢ مليون US$"); + expect(ar.format(1290000)).toBe("١٫٣ مليون US$"); + expect(ar.format(12000000)).toBe("١٢ مليون US$"); + expect(ar.format(12900000)).toBe("١٣ مليون US$"); const ja = new Intl.NumberFormat("ja", { style: "currency", diff --git a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatToParts.js b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatToParts.js index 8836163caa5..c102e1c238b 100644 --- a/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatToParts.js +++ b/Userland/Libraries/LibJS/Tests/builtins/Intl/NumberFormat/NumberFormat.prototype.formatToParts.js @@ -36,7 +36,8 @@ describe("special values", () => { const ar = new Intl.NumberFormat("ar"); expect(ar.formatToParts(Infinity)).toEqual([{ type: "infinity", value: "∞" }]); expect(ar.formatToParts(-Infinity)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "infinity", value: "∞" }, ]); }); @@ -98,11 +99,13 @@ describe("style=decimal", () => { expect(ar.formatToParts(0)).toEqual([{ type: "integer", value: "\u0660" }]); expect(ar.formatToParts(1)).toEqual([{ type: "integer", value: "\u0661" }]); expect(ar.formatToParts(-0)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "integer", value: "\u0660" }, ]); expect(ar.formatToParts(-1)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "integer", value: "\u0661" }, ]); }); @@ -128,19 +131,23 @@ describe("style=decimal", () => { const ar = new Intl.NumberFormat("ar", { signDisplay: "always" }); expect(ar.formatToParts(0)).toEqual([ - { type: "plusSign", value: "\u061c+" }, + { type: "literal", value: "\u061c" }, + { type: "plusSign", value: "+" }, { type: "integer", value: "\u0660" }, ]); expect(ar.formatToParts(1)).toEqual([ - { type: "plusSign", value: "\u061c+" }, + { type: "literal", value: "\u061c" }, + { type: "plusSign", value: "+" }, { type: "integer", value: "\u0661" }, ]); expect(ar.formatToParts(-0)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "integer", value: "\u0660" }, ]); expect(ar.formatToParts(-1)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "integer", value: "\u0661" }, ]); }); @@ -161,12 +168,14 @@ describe("style=decimal", () => { const ar = new Intl.NumberFormat("ar", { signDisplay: "exceptZero" }); expect(ar.formatToParts(0)).toEqual([{ type: "integer", value: "\u0660" }]); expect(ar.formatToParts(1)).toEqual([ - { type: "plusSign", value: "\u061c+" }, + { type: "literal", value: "\u061c" }, + { type: "plusSign", value: "+" }, { type: "integer", value: "\u0661" }, ]); expect(ar.formatToParts(-0)).toEqual([{ type: "integer", value: "\u0660" }]); expect(ar.formatToParts(-1)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "integer", value: "\u0661" }, ]); }); @@ -186,7 +195,8 @@ describe("style=decimal", () => { expect(ar.formatToParts(1)).toEqual([{ type: "integer", value: "\u0661" }]); expect(ar.formatToParts(-0)).toEqual([{ type: "integer", value: "\u0660" }]); expect(ar.formatToParts(-1)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "integer", value: "\u0661" }, ]); }); @@ -330,7 +340,8 @@ describe("style=decimal", () => { { type: "decimal", value: "\u066b" }, { type: "fraction", value: "\u0662" }, { type: "exponentSeparator", value: "\u0623\u0633" }, - { type: "exponentMinusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "exponentMinusSign", value: "-" }, { type: "exponentInteger", value: "\u0661" }, ]); }); @@ -362,7 +373,8 @@ describe("style=decimal", () => { expect(ar.formatToParts(0.12)).toEqual([ { type: "integer", value: "\u0661\u0662\u0660" }, { type: "exponentSeparator", value: "\u0623\u0633" }, - { type: "exponentMinusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "exponentMinusSign", value: "-" }, { type: "exponentInteger", value: "\u0663" }, ]); }); @@ -447,13 +459,15 @@ describe("style=percent", () => { { type: "integer", value: "\u0661\u0660\u0660" }, { type: "decimal", value: "\u066b" }, { type: "fraction", value: "\u0660\u0660" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(1.2345)).toEqual([ { type: "integer", value: "\u0661\u0662\u0663" }, { type: "decimal", value: "\u066b" }, { type: "fraction", value: "\u0664\u0665" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); }); @@ -471,11 +485,13 @@ describe("style=percent", () => { const ar = new Intl.NumberFormat("ar", { style: "percent", signDisplay: "never" }); expect(ar.formatToParts(0.01)).toEqual([ { type: "integer", value: "\u0661" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(-0.01)).toEqual([ { type: "integer", value: "\u0661" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); }); @@ -503,21 +519,27 @@ describe("style=percent", () => { const ar = new Intl.NumberFormat("ar", { style: "percent", signDisplay: "auto" }); expect(ar.formatToParts(0.0)).toEqual([ { type: "integer", value: "\u0660" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(0.01)).toEqual([ { type: "integer", value: "\u0661" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(-0.0)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "integer", value: "\u0660" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(-0.01)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "integer", value: "\u0661" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); }); @@ -546,24 +568,32 @@ describe("style=percent", () => { const ar = new Intl.NumberFormat("ar", { style: "percent", signDisplay: "always" }); expect(ar.formatToParts(0.0)).toEqual([ - { type: "plusSign", value: "\u061c+" }, + { type: "literal", value: "\u061c" }, + { type: "plusSign", value: "+" }, { type: "integer", value: "\u0660" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(0.01)).toEqual([ - { type: "plusSign", value: "\u061c+" }, + { type: "literal", value: "\u061c" }, + { type: "plusSign", value: "+" }, { type: "integer", value: "\u0661" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(-0.0)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "integer", value: "\u0660" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(-0.01)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "integer", value: "\u0661" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); }); @@ -591,21 +621,27 @@ describe("style=percent", () => { const ar = new Intl.NumberFormat("ar", { style: "percent", signDisplay: "exceptZero" }); expect(ar.formatToParts(0.0)).toEqual([ { type: "integer", value: "\u0660" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(0.01)).toEqual([ - { type: "plusSign", value: "\u061c+" }, + { type: "literal", value: "\u061c" }, + { type: "plusSign", value: "+" }, { type: "integer", value: "\u0661" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(-0.0)).toEqual([ { type: "integer", value: "\u0660" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(-0.01)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "integer", value: "\u0661" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); }); @@ -632,20 +668,25 @@ describe("style=percent", () => { const ar = new Intl.NumberFormat("ar", { style: "percent", signDisplay: "negative" }); expect(ar.formatToParts(0.0)).toEqual([ { type: "integer", value: "\u0660" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(0.01)).toEqual([ { type: "integer", value: "\u0661" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(-0.0)).toEqual([ { type: "integer", value: "\u0660" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); expect(ar.formatToParts(-0.01)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "integer", value: "\u0661" }, - { type: "percentSign", value: "\u066a\u061c" }, + { type: "percentSign", value: "\u066a" }, + { type: "literal", value: "\u061c" }, ]); }); }); @@ -937,7 +978,8 @@ describe("style=currency", () => { { type: "currency", value: "US$" }, ]); expect(ar.formatToParts(-0)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "literal", value: "\u200f" }, { type: "integer", value: "\u0660" }, { type: "decimal", value: "\u066b" }, @@ -946,7 +988,8 @@ describe("style=currency", () => { { type: "currency", value: "US$" }, ]); expect(ar.formatToParts(-1)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "literal", value: "\u200f" }, { type: "integer", value: "\u0661" }, { type: "decimal", value: "\u066b" }, @@ -1032,7 +1075,8 @@ describe("style=currency", () => { signDisplay: "always", }); expect(ar.formatToParts(0)).toEqual([ - { type: "plusSign", value: "\u061c+" }, + { type: "literal", value: "\u061c" }, + { type: "plusSign", value: "+" }, { type: "literal", value: "\u200f" }, { type: "integer", value: "\u0660" }, { type: "decimal", value: "\u066b" }, @@ -1041,7 +1085,8 @@ describe("style=currency", () => { { type: "currency", value: "US$" }, ]); expect(ar.formatToParts(1)).toEqual([ - { type: "plusSign", value: "\u061c+" }, + { type: "literal", value: "\u061c" }, + { type: "plusSign", value: "+" }, { type: "literal", value: "\u200f" }, { type: "integer", value: "\u0661" }, { type: "decimal", value: "\u066b" }, @@ -1050,7 +1095,8 @@ describe("style=currency", () => { { type: "currency", value: "US$" }, ]); expect(ar.formatToParts(-0)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "literal", value: "\u200f" }, { type: "integer", value: "\u0660" }, { type: "decimal", value: "\u066b" }, @@ -1059,7 +1105,8 @@ describe("style=currency", () => { { type: "currency", value: "US$" }, ]); expect(ar.formatToParts(-1)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "literal", value: "\u200f" }, { type: "integer", value: "\u0661" }, { type: "decimal", value: "\u066b" }, @@ -1153,7 +1200,8 @@ describe("style=currency", () => { { type: "currency", value: "US$" }, ]); expect(ar.formatToParts(1)).toEqual([ - { type: "plusSign", value: "\u061c+" }, + { type: "literal", value: "\u061c" }, + { type: "plusSign", value: "+" }, { type: "literal", value: "\u200f" }, { type: "integer", value: "\u0661" }, { type: "decimal", value: "\u066b" }, @@ -1170,7 +1218,8 @@ describe("style=currency", () => { { type: "currency", value: "US$" }, ]); expect(ar.formatToParts(-1)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "literal", value: "\u200f" }, { type: "integer", value: "\u0661" }, { type: "decimal", value: "\u066b" }, @@ -1276,7 +1325,8 @@ describe("style=currency", () => { { type: "currency", value: "US$" }, ]); expect(ar.formatToParts(-1)).toEqual([ - { type: "minusSign", value: "\u061c-" }, + { type: "literal", value: "\u061c" }, + { type: "minusSign", value: "-" }, { type: "literal", value: "\u200f" }, { type: "integer", value: "\u0661" }, { type: "decimal", value: "\u066b" }, diff --git a/Userland/Libraries/LibLocale/Forward.h b/Userland/Libraries/LibLocale/Forward.h index f6abe2645c4..b63658a0f05 100644 --- a/Userland/Libraries/LibLocale/Forward.h +++ b/Userland/Libraries/LibLocale/Forward.h @@ -13,7 +13,6 @@ namespace Locale { enum class CalendarFormatType : u8; enum class CalendarPatternStyle : u8; enum class CalendarSymbol : u8; -enum class CompactNumberFormatType : u8; enum class DayPeriod : u8; enum class Era : u8; enum class FirstDayRegion : u8; @@ -31,12 +30,13 @@ enum class MinimumDaysRegion : u8; enum class Month : u8; enum class NumericSymbol : u8; enum class PluralCategory : u8; -enum class StandardNumberFormatType : u8; enum class Style : u8; enum class Weekday : u8; enum class WeekendEndRegion : u8; enum class WeekendStartRegion : u8; +class NumberFormat; + struct CalendarFormat; struct CalendarPattern; struct CalendarRangePattern; @@ -45,8 +45,6 @@ struct LanguageID; struct ListFormatPart; struct LocaleExtension; struct LocaleID; -struct NumberFormat; -struct NumberGroupings; struct OtherExtension; struct PluralOperands; struct TransformedExtension; diff --git a/Userland/Libraries/LibLocale/NumberFormat.cpp b/Userland/Libraries/LibLocale/NumberFormat.cpp index 286bdcda67d..a39551d38d5 100644 --- a/Userland/Libraries/LibLocale/NumberFormat.cpp +++ b/Userland/Libraries/LibLocale/NumberFormat.cpp @@ -1,14 +1,21 @@ /* - * Copyright (c) 2021-2023, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ +#define AK_DONT_REPLACE_STD + #include +#include #include +#include #include #include #include +#include + +#include #if ENABLE_UNICODE_DATA # include @@ -16,11 +23,718 @@ namespace Locale { +NumberFormatStyle number_format_style_from_string(StringView number_format_style) +{ + if (number_format_style == "decimal"sv) + return NumberFormatStyle::Decimal; + if (number_format_style == "percent"sv) + return NumberFormatStyle::Percent; + if (number_format_style == "currency"sv) + return NumberFormatStyle::Currency; + if (number_format_style == "unit"sv) + return NumberFormatStyle::Unit; + VERIFY_NOT_REACHED(); +} + +StringView number_format_style_to_string(NumberFormatStyle number_format_style) +{ + switch (number_format_style) { + case NumberFormatStyle::Decimal: + return "decimal"sv; + case NumberFormatStyle::Percent: + return "percent"sv; + case NumberFormatStyle::Currency: + return "currency"sv; + case NumberFormatStyle::Unit: + return "unit"sv; + } + VERIFY_NOT_REACHED(); +} + +SignDisplay sign_display_from_string(StringView sign_display) +{ + if (sign_display == "auto"sv) + return SignDisplay::Auto; + if (sign_display == "never"sv) + return SignDisplay::Never; + if (sign_display == "always"sv) + return SignDisplay::Always; + if (sign_display == "exceptZero"sv) + return SignDisplay::ExceptZero; + if (sign_display == "negative"sv) + return SignDisplay::Negative; + VERIFY_NOT_REACHED(); +} + +StringView sign_display_to_string(SignDisplay sign_display) +{ + switch (sign_display) { + case SignDisplay::Auto: + return "auto"sv; + case SignDisplay::Never: + return "never"sv; + case SignDisplay::Always: + return "always"sv; + case SignDisplay::ExceptZero: + return "exceptZero"sv; + case SignDisplay::Negative: + return "negative"sv; + } + VERIFY_NOT_REACHED(); +} + +static constexpr UNumberSignDisplay icu_sign_display(SignDisplay sign_display, Optional const& currency_sign) +{ + switch (sign_display) { + case SignDisplay::Auto: + return currency_sign == CurrencySign::Standard ? UNUM_SIGN_AUTO : UNUM_SIGN_ACCOUNTING; + case SignDisplay::Never: + return UNUM_SIGN_NEVER; + case SignDisplay::Always: + return currency_sign == CurrencySign::Standard ? UNUM_SIGN_ALWAYS : UNUM_SIGN_ACCOUNTING_ALWAYS; + case SignDisplay::ExceptZero: + return currency_sign == CurrencySign::Standard ? UNUM_SIGN_EXCEPT_ZERO : UNUM_SIGN_ACCOUNTING_EXCEPT_ZERO; + case SignDisplay::Negative: + return currency_sign == CurrencySign::Standard ? UNUM_SIGN_NEGATIVE : UNUM_SIGN_ACCOUNTING_NEGATIVE; + } + VERIFY_NOT_REACHED(); +} + +Notation notation_from_string(StringView notation) +{ + if (notation == "standard"sv) + return Notation::Standard; + if (notation == "scientific"sv) + return Notation::Scientific; + if (notation == "engineering"sv) + return Notation::Engineering; + if (notation == "compact"sv) + return Notation::Compact; + VERIFY_NOT_REACHED(); +} + +StringView notation_to_string(Notation notation) +{ + switch (notation) { + case Notation::Standard: + return "standard"sv; + case Notation::Scientific: + return "scientific"sv; + case Notation::Engineering: + return "engineering"sv; + case Notation::Compact: + return "compact"sv; + } + VERIFY_NOT_REACHED(); +} + +static icu::number::Notation icu_notation(Notation notation, Optional const& compact_display) +{ + switch (notation) { + case Notation::Standard: + return icu::number::Notation::simple(); + case Notation::Scientific: + return icu::number::Notation::scientific(); + case Notation::Engineering: + return icu::number::Notation::engineering(); + case Notation::Compact: + switch (*compact_display) { + case CompactDisplay::Short: + return icu::number::Notation::compactShort(); + case CompactDisplay::Long: + return icu::number::Notation::compactLong(); + } + } + VERIFY_NOT_REACHED(); +} + +CompactDisplay compact_display_from_string(StringView compact_display) +{ + if (compact_display == "short"sv) + return CompactDisplay::Short; + if (compact_display == "long"sv) + return CompactDisplay::Long; + VERIFY_NOT_REACHED(); +} + +StringView compact_display_to_string(CompactDisplay compact_display) +{ + switch (compact_display) { + case CompactDisplay::Short: + return "short"sv; + case CompactDisplay::Long: + return "long"sv; + } + VERIFY_NOT_REACHED(); +} + +Grouping grouping_from_string(StringView grouping) +{ + if (grouping == "always"sv) + return Grouping::Always; + if (grouping == "auto"sv) + return Grouping::Auto; + if (grouping == "min2"sv) + return Grouping::Min2; + if (grouping == "false"sv) + return Grouping::False; + VERIFY_NOT_REACHED(); +} + +StringView grouping_to_string(Grouping grouping) +{ + switch (grouping) { + case Grouping::Always: + return "always"sv; + case Grouping::Auto: + return "auto"sv; + case Grouping::Min2: + return "min2"sv; + case Grouping::False: + return "false"sv; + } + VERIFY_NOT_REACHED(); +} + +static constexpr UNumberGroupingStrategy icu_grouping_strategy(Grouping grouping) +{ + switch (grouping) { + case Grouping::Always: + return UNUM_GROUPING_ON_ALIGNED; + case Grouping::Auto: + return UNUM_GROUPING_AUTO; + case Grouping::Min2: + return UNUM_GROUPING_MIN2; + case Grouping::False: + return UNUM_GROUPING_OFF; + } + VERIFY_NOT_REACHED(); +} + +CurrencyDisplay currency_display_from_string(StringView currency_display) +{ + if (currency_display == "code"sv) + return CurrencyDisplay::Code; + if (currency_display == "symbol"sv) + return CurrencyDisplay::Symbol; + if (currency_display == "narrowSymbol"sv) + return CurrencyDisplay::NarrowSymbol; + if (currency_display == "name"sv) + return CurrencyDisplay::Name; + VERIFY_NOT_REACHED(); +} + +StringView currency_display_to_string(CurrencyDisplay currency_display) +{ + switch (currency_display) { + case CurrencyDisplay::Code: + return "code"sv; + case CurrencyDisplay::Symbol: + return "symbol"sv; + case CurrencyDisplay::NarrowSymbol: + return "narrowSymbol"sv; + case CurrencyDisplay::Name: + return "name"sv; + } + VERIFY_NOT_REACHED(); +} + +static constexpr UNumberUnitWidth icu_currency_display(CurrencyDisplay currency_display) +{ + switch (currency_display) { + case CurrencyDisplay::Code: + return UNUM_UNIT_WIDTH_ISO_CODE; + case CurrencyDisplay::Symbol: + return UNUM_UNIT_WIDTH_SHORT; + case CurrencyDisplay::NarrowSymbol: + return UNUM_UNIT_WIDTH_NARROW; + case CurrencyDisplay::Name: + return UNUM_UNIT_WIDTH_FULL_NAME; + } + VERIFY_NOT_REACHED(); +} + +CurrencySign currency_sign_from_string(StringView currency_sign) +{ + if (currency_sign == "standard"sv) + return CurrencySign::Standard; + if (currency_sign == "accounting"sv) + return CurrencySign::Accounting; + VERIFY_NOT_REACHED(); +} + +StringView currency_sign_to_string(CurrencySign currency_sign) +{ + switch (currency_sign) { + case CurrencySign::Standard: + return "standard"sv; + case CurrencySign::Accounting: + return "accounting"sv; + } + VERIFY_NOT_REACHED(); +} + +RoundingType rounding_type_from_string(StringView rounding_type) +{ + if (rounding_type == "significantDigits"sv) + return RoundingType::SignificantDigits; + if (rounding_type == "fractionDigits"sv) + return RoundingType::FractionDigits; + if (rounding_type == "morePrecision"sv) + return RoundingType::MorePrecision; + if (rounding_type == "lessPrecision"sv) + return RoundingType::LessPrecision; + VERIFY_NOT_REACHED(); +} + +StringView rounding_type_to_string(RoundingType rounding_type) +{ + switch (rounding_type) { + case RoundingType::SignificantDigits: + return "significantDigits"sv; + case RoundingType::FractionDigits: + return "fractionDigits"sv; + case RoundingType::MorePrecision: + return "morePrecision"sv; + case RoundingType::LessPrecision: + return "lessPrecision"sv; + } + VERIFY_NOT_REACHED(); +} + +RoundingMode rounding_mode_from_string(StringView rounding_mode) +{ + if (rounding_mode == "ceil"sv) + return RoundingMode::Ceil; + if (rounding_mode == "expand"sv) + return RoundingMode::Expand; + if (rounding_mode == "floor"sv) + return RoundingMode::Floor; + if (rounding_mode == "halfCeil"sv) + return RoundingMode::HalfCeil; + if (rounding_mode == "halfEven"sv) + return RoundingMode::HalfEven; + if (rounding_mode == "halfExpand"sv) + return RoundingMode::HalfExpand; + if (rounding_mode == "halfFloor"sv) + return RoundingMode::HalfFloor; + if (rounding_mode == "halfTrunc"sv) + return RoundingMode::HalfTrunc; + if (rounding_mode == "trunc"sv) + return RoundingMode::Trunc; + VERIFY_NOT_REACHED(); +} + +StringView rounding_mode_to_string(RoundingMode rounding_mode) +{ + switch (rounding_mode) { + case RoundingMode::Ceil: + return "ceil"sv; + case RoundingMode::Expand: + return "expand"sv; + case RoundingMode::Floor: + return "floor"sv; + case RoundingMode::HalfCeil: + return "halfCeil"sv; + case RoundingMode::HalfEven: + return "halfEven"sv; + case RoundingMode::HalfExpand: + return "halfExpand"sv; + case RoundingMode::HalfFloor: + return "halfFloor"sv; + case RoundingMode::HalfTrunc: + return "halfTrunc"sv; + case RoundingMode::Trunc: + return "trunc"sv; + } + VERIFY_NOT_REACHED(); +} + +static constexpr UNumberFormatRoundingMode icu_rounding_mode(RoundingMode rounding_mode) +{ + switch (rounding_mode) { + case RoundingMode::Ceil: + return UNUM_ROUND_CEILING; + case RoundingMode::Expand: + return UNUM_ROUND_UP; + case RoundingMode::Floor: + return UNUM_ROUND_FLOOR; + case RoundingMode::HalfCeil: + return UNUM_ROUND_HALF_CEILING; + case RoundingMode::HalfEven: + return UNUM_ROUND_HALFEVEN; + case RoundingMode::HalfExpand: + return UNUM_ROUND_HALFUP; + case RoundingMode::HalfFloor: + return UNUM_ROUND_HALF_FLOOR; + case RoundingMode::HalfTrunc: + return UNUM_ROUND_HALFDOWN; + case RoundingMode::Trunc: + return UNUM_ROUND_DOWN; + } + VERIFY_NOT_REACHED(); +} + +TrailingZeroDisplay trailing_zero_display_from_string(StringView trailing_zero_display) +{ + if (trailing_zero_display == "auto"sv) + return TrailingZeroDisplay::Auto; + if (trailing_zero_display == "stripIfInteger"sv) + return TrailingZeroDisplay::StripIfInteger; + VERIFY_NOT_REACHED(); +} + +StringView trailing_zero_display_to_string(TrailingZeroDisplay trailing_zero_display) +{ + switch (trailing_zero_display) { + case TrailingZeroDisplay::Auto: + return "auto"sv; + case TrailingZeroDisplay::StripIfInteger: + return "stripIfInteger"sv; + } + VERIFY_NOT_REACHED(); +} + +static constexpr UNumberTrailingZeroDisplay icu_trailing_zero_display(TrailingZeroDisplay trailing_zero_display) +{ + switch (trailing_zero_display) { + case TrailingZeroDisplay::Auto: + return UNUM_TRAILING_ZERO_AUTO; + case TrailingZeroDisplay::StripIfInteger: + return UNUM_TRAILING_ZERO_HIDE_IF_WHOLE; + } + VERIFY_NOT_REACHED(); +} + +static constexpr UNumberUnitWidth icu_unit_width(Style unit_display) +{ + switch (unit_display) { + case Style::Long: + return UNUM_UNIT_WIDTH_FULL_NAME; + case Style::Short: + return UNUM_UNIT_WIDTH_SHORT; + case Style::Narrow: + return UNUM_UNIT_WIDTH_NARROW; + } + VERIFY_NOT_REACHED(); +} + +static void apply_display_options(icu::number::LocalizedNumberFormatter& formatter, DisplayOptions const& display_options) +{ + UErrorCode status = U_ZERO_ERROR; + + switch (display_options.style) { + case NumberFormatStyle::Decimal: + break; + + case NumberFormatStyle::Percent: + formatter = formatter.unit(icu::MeasureUnit::getPercent()).scale(icu::number::Scale::byDouble(100)); + break; + + case NumberFormatStyle::Currency: + formatter = formatter.unit(icu::CurrencyUnit(icu_string_piece(*display_options.currency), status)); + formatter = formatter.unitWidth(icu_currency_display(*display_options.currency_display)); + VERIFY(icu_success(status)); + break; + + case NumberFormatStyle::Unit: + formatter = formatter.unit(icu::MeasureUnit::forIdentifier(icu_string_piece(*display_options.unit), status)); + formatter = formatter.unitWidth(icu_unit_width(*display_options.unit_display)); + VERIFY(icu_success(status)); + break; + } + + formatter = formatter.sign(icu_sign_display(display_options.sign_display, display_options.currency_sign)); + formatter = formatter.notation(icu_notation(display_options.notation, display_options.compact_display)); + formatter = formatter.grouping(icu_grouping_strategy(display_options.grouping)); +} + +static void apply_rounding_options(icu::number::LocalizedNumberFormatter& formatter, RoundingOptions const& rounding_options) +{ + auto precision = icu::number::Precision::unlimited(); + + if (rounding_options.rounding_increment == 1) { + switch (rounding_options.type) { + case RoundingType::SignificantDigits: + precision = icu::number::Precision::minMaxSignificantDigits(*rounding_options.min_significant_digits, *rounding_options.max_significant_digits); + break; + case RoundingType::FractionDigits: + precision = icu::number::Precision::minMaxFraction(*rounding_options.min_fraction_digits, *rounding_options.max_fraction_digits); + break; + case RoundingType::MorePrecision: + precision = icu::number::Precision::minMaxFraction(*rounding_options.min_fraction_digits, *rounding_options.max_fraction_digits) + .withSignificantDigits(*rounding_options.min_significant_digits, *rounding_options.max_significant_digits, UNUM_ROUNDING_PRIORITY_RELAXED); + break; + case RoundingType::LessPrecision: + precision = icu::number::Precision::minMaxFraction(*rounding_options.min_fraction_digits, *rounding_options.max_fraction_digits) + .withSignificantDigits(*rounding_options.min_significant_digits, *rounding_options.max_significant_digits, UNUM_ROUNDING_PRIORITY_STRICT); + break; + } + } else { + auto mantissa = rounding_options.rounding_increment; + auto magnitude = *rounding_options.max_fraction_digits * -1; + + precision = icu::number::Precision::incrementExact(mantissa, static_cast(magnitude)) + .withMinFraction(*rounding_options.min_fraction_digits); + } + + formatter = formatter.precision(precision.trailingZeroDisplay(icu_trailing_zero_display(rounding_options.trailing_zero_display))); + formatter = formatter.integerWidth(icu::number::IntegerWidth::zeroFillTo(rounding_options.min_integer_digits)); + formatter = formatter.roundingMode(icu_rounding_mode(rounding_options.mode)); +} + +// ICU does not contain a field enumeration for "literal" partitions. Define a custom field so that we may provide a +// type for those partitions. +static constexpr i32 LITERAL_FIELD = -1; + +static constexpr StringView icu_number_format_field_to_string(i32 field, NumberFormat::Value const& value, bool is_unit) +{ + switch (field) { + case LITERAL_FIELD: + return "literal"sv; + case UNUM_INTEGER_FIELD: + if (auto const* number = value.get_pointer()) { + if (isnan(*number)) + return "nan"sv; + if (isinf(*number)) + return "infinity"sv; + } + return "integer"sv; + case UNUM_FRACTION_FIELD: + return "fraction"sv; + case UNUM_DECIMAL_SEPARATOR_FIELD: + return "decimal"sv; + case UNUM_EXPONENT_SYMBOL_FIELD: + return "exponentSeparator"sv; + case UNUM_EXPONENT_SIGN_FIELD: + return "exponentMinusSign"sv; + case UNUM_EXPONENT_FIELD: + return "exponentInteger"sv; + case UNUM_GROUPING_SEPARATOR_FIELD: + return "group"sv; + case UNUM_CURRENCY_FIELD: + return "currency"sv; + case UNUM_PERCENT_FIELD: + return is_unit ? "unit"sv : "percentSign"sv; + case UNUM_SIGN_FIELD: { + auto is_negative = value.visit( + [&](double number) { return signbit(number); }, + [&](String const& number) { return number.starts_with('-'); }); + return is_negative ? "minusSign"sv : "plusSign"sv; + } + case UNUM_MEASURE_UNIT_FIELD: + return "unit"sv; + case UNUM_COMPACT_FIELD: + return "compact"sv; + case UNUM_APPROXIMATELY_SIGN_FIELD: + return "approximatelySign"sv; + } + + VERIFY_NOT_REACHED(); +} + +struct Range { + constexpr bool operator<(Range const& other) const + { + if (start < other.start) + return true; + if (start == other.start) + return end > other.end; + return false; + } + + i32 field { LITERAL_FIELD }; + i32 start { 0 }; + i32 end { 0 }; +}; + +// ICU will give us overlapping partitions, e.g. for the formatted result "1,234", we will get the following parts: +// +// part="," type=group start=1 end=2 +// part="1,234" type=integer start=0 end=5 +// +// We need to massage these partitions into non-overlapping parts for ECMA-402: +// +// part="1" type=integer start=0 end=1 +// part="," type=group start=1 end=2 +// part="234" type=integer start=2 end=5 +static void flatten_partitions(Vector& partitions) +{ + if (partitions.size() <= 1) + return; + + quick_sort(partitions); + + auto subtract_range = [&](auto const& first, auto const& second) -> Vector { + if (second.start > first.end || first.start > second.end) + return { first }; + + Vector result; + + if (second.start > first.start) + result.empend(first.field, first.start, second.start); + if (second.end < first.end) + result.empend(first.field, second.end, first.end); + + return result; + }; + + for (size_t i = 0; i < partitions.size(); ++i) { + for (size_t j = i + 1; j < partitions.size(); ++j) { + auto& first = partitions[i]; + auto& second = partitions[j]; + + auto result = subtract_range(first, second); + + if (result.is_empty()) { + partitions.remove(i); + --i; + break; + } + + first = result[0]; + + if (result.size() == 2) + partitions.insert(i + 1, result[1]); + } + } + + quick_sort(partitions); +} + +class NumberFormatImpl : public NumberFormat { +public: + NumberFormatImpl(icu::number::LocalizedNumberFormatter formatter, bool is_unit) + : m_formatter(move(formatter)) + , m_is_unit(is_unit) + { + } + + virtual ~NumberFormatImpl() override = default; + + virtual String format(Value const& value) const override + { + UErrorCode status = U_ZERO_ERROR; + + auto formatted = format_impl(value); + if (!formatted.has_value()) + return {}; + + auto result = formatted->toTempString(status); + if (icu_failure(status)) + return {}; + + return icu_string_to_string(result); + } + + virtual String format_to_decimal(Value const& value) const override + { + UErrorCode status = U_ZERO_ERROR; + + auto formatted = format_impl(value); + if (!formatted.has_value()) + return {}; + + auto result = formatted->toDecimalNumber(status); + if (icu_failure(status)) + return {}; + + return MUST(result.to_string()); + } + + virtual Vector format_to_parts(Value const& value) const override + { + auto formatted = format_impl(value); + if (!formatted.has_value()) + return {}; + + return format_to_parts_impl(formatted, value); + } + +private: + Optional format_impl(Value const& value) const + { + UErrorCode status = U_ZERO_ERROR; + + auto formatted = value.visit( + [&](double number) { + return m_formatter.formatDouble(number, status); + }, + [&](String const& number) { + return m_formatter.formatDecimal(icu_string_piece(number), status); + }); + + if (icu_failure(status)) + return {}; + + return formatted; + } + + template + Vector format_to_parts_impl(Formatted const& formatted, Value const& value) const + { + UErrorCode status = U_ZERO_ERROR; + + auto formatted_number = formatted->toTempString(status); + if (icu_failure(status)) + return {}; + + Vector ranges; + ranges.empend(LITERAL_FIELD, 0, formatted_number.length()); + + icu::ConstrainedFieldPosition position; + + while (static_cast(formatted->nextPosition(position, status)) && icu_success(status)) { + ranges.empend(position.getField(), position.getStart(), position.getLimit()); + } + + flatten_partitions(ranges); + + Vector result; + result.ensure_capacity(ranges.size()); + + for (auto const& range : ranges) { + auto string = formatted_number.tempSubStringBetween(range.start, range.end); + + Partition partition; + partition.type = icu_number_format_field_to_string(range.field, value, m_is_unit); + partition.value = icu_string_to_string(string); + + result.unchecked_append(move(partition)); + } + + return result; + } + + icu::number::LocalizedNumberFormatter m_formatter; + bool m_is_unit { false }; +}; + +NonnullOwnPtr NumberFormat::create( + StringView locale, + StringView numbering_system, + DisplayOptions const& display_options, + RoundingOptions const& rounding_options) +{ + UErrorCode status = U_ZERO_ERROR; + + auto locale_data = LocaleData::for_locale(locale); + VERIFY(locale_data.has_value()); + + auto formatter = icu::number::NumberFormatter::withLocale(locale_data->locale()); + apply_display_options(formatter, display_options); + apply_rounding_options(formatter, rounding_options); + + if (!numbering_system.is_empty()) { + if (auto* symbols = icu::NumberingSystem::createInstanceByName(ByteString(numbering_system).characters(), status); symbols && icu_success(status)) + formatter = formatter.adoptSymbols(symbols); + } + + bool is_unit = display_options.style == NumberFormatStyle::Unit; + return adopt_own(*new NumberFormatImpl(move(formatter), is_unit)); +} + Optional __attribute__((weak)) get_number_system_symbol(StringView, StringView, NumericSymbol) { return {}; } -Optional __attribute__((weak)) get_number_system_groupings(StringView, StringView) { return {}; } -Optional __attribute__((weak)) get_standard_number_system_format(StringView, StringView, StandardNumberFormatType) { return {}; } -Vector __attribute__((weak)) get_compact_number_system_formats(StringView, StringView, CompactNumberFormatType) { return {}; } -Vector __attribute__((weak)) get_unit_formats(StringView, StringView, Style) { return {}; } Optional> __attribute__((weak)) get_digits_for_number_system(StringView) { @@ -63,50 +777,6 @@ static u32 last_code_point(StringView string) } #endif -// https://www.unicode.org/reports/tr35/tr35-numbers.html#Currencies -Optional augment_currency_format_pattern([[maybe_unused]] StringView currency_display, [[maybe_unused]] StringView base_pattern) -{ -#if ENABLE_UNICODE_DATA - 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()); - - Utf8View utf8_currency_display { currency_display }; - Optional currency_key_with_spacing; - - if (*number_index < *currency_index) { - u32 last_pattern_code_point = last_code_point(base_pattern.substring_view(0, *currency_index)); - - if (!Unicode::code_point_has_general_category(last_pattern_code_point, Unicode::GeneralCategory::Separator)) { - u32 first_currency_code_point = *utf8_currency_display.begin(); - - if (!Unicode::code_point_has_general_category(first_currency_code_point, Unicode::GeneralCategory::Symbol)) - currency_key_with_spacing = MUST(String::formatted("{}{}", spacing, currency_key)); - } - } else { - u32 last_pattern_code_point = last_code_point(base_pattern.substring_view(0, *number_index)); - - if (!Unicode::code_point_has_general_category(last_pattern_code_point, Unicode::GeneralCategory::Separator)) { - u32 last_currency_code_point = last_code_point(currency_display); - - if (!Unicode::code_point_has_general_category(last_currency_code_point, Unicode::GeneralCategory::Symbol)) - currency_key_with_spacing = MUST(String::formatted("{}{}", currency_key, spacing)); - } - } - - if (currency_key_with_spacing.has_value()) - return MUST(MUST(String::from_utf8(base_pattern)).replace(currency_key, *currency_key_with_spacing, ReplaceMode::FirstOnly)); -#endif - - return {}; -} - // https://unicode.org/reports/tr35/tr35-numbers.html#83-range-pattern-processing Optional augment_range_pattern([[maybe_unused]] StringView range_separator, [[maybe_unused]] StringView lower, [[maybe_unused]] StringView upper) { diff --git a/Userland/Libraries/LibLocale/NumberFormat.h b/Userland/Libraries/LibLocale/NumberFormat.h index 4ded64904b7..066e979c804 100644 --- a/Userland/Libraries/LibLocale/NumberFormat.h +++ b/Userland/Libraries/LibLocale/NumberFormat.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -9,40 +9,158 @@ #include #include #include +#include #include #include #include namespace Locale { -struct NumberGroupings { - u8 minimum_grouping_digits { 0 }; - u8 primary_grouping_size { 0 }; - u8 secondary_grouping_size { 0 }; -}; - -enum class StandardNumberFormatType : u8 { +enum class NumberFormatStyle { Decimal, - Currency, - Accounting, Percent, + Currency, + Unit, +}; +NumberFormatStyle number_format_style_from_string(StringView); +StringView number_format_style_to_string(NumberFormatStyle); + +enum class SignDisplay { + Auto, + Never, + Always, + ExceptZero, + Negative, +}; +SignDisplay sign_display_from_string(StringView); +StringView sign_display_to_string(SignDisplay); + +enum class Notation { + Standard, Scientific, + Engineering, + Compact, +}; +Notation notation_from_string(StringView); +StringView notation_to_string(Notation); + +enum class CompactDisplay { + Short, + Long, +}; +CompactDisplay compact_display_from_string(StringView); +StringView compact_display_to_string(CompactDisplay); + +enum class Grouping { + Always, + Auto, + Min2, + False, +}; +Grouping grouping_from_string(StringView); +StringView grouping_to_string(Grouping); + +enum class CurrencyDisplay { + Code, + Symbol, + NarrowSymbol, + Name, +}; +CurrencyDisplay currency_display_from_string(StringView); +StringView currency_display_to_string(CurrencyDisplay); + +enum class CurrencySign { + Standard, + Accounting, +}; +CurrencySign currency_sign_from_string(StringView); +StringView currency_sign_to_string(CurrencySign); + +struct DisplayOptions { + NumberFormatStyle style { NumberFormatStyle::Decimal }; + SignDisplay sign_display { SignDisplay::Auto }; + + Notation notation { Notation::Standard }; + Optional compact_display; + + Grouping grouping { Grouping::Always }; + + Optional currency; + Optional currency_display; + Optional currency_sign; + + Optional unit; + Optional