diff --git a/Meta/CMake/locale_data.cmake b/Meta/CMake/locale_data.cmake index ec3ce56c1b..1d907046b7 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 ff9d636026..0a5b79c603 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 32ff899648..6047a0d6fa 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 ea1593adf6..5c0a686c6b 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 d163caa96d..a85ba1227c 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 073f1719f4..4576ccf2ac 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 8b1e9c3b16..d3c89e0341 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 8547c82272..e3a6c5d396 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 e03c9a984f..6eae17152c 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 55dc2bc693..ca05c0af92 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 0ef735c9a1..12ed3f3773 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 a3cd15b8f3..32e9aaab7d 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 80e64d48f7..737a50c0b6 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 b67a8b2d0f..b9077b6fa9 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 e9ec20e590..d5a9058ac0 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 d9d1e8bb19..1f4c97290e 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 9149749bf4..1f752c04ac 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 568c9e43d3..be07b0e05f 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 dd06de327b..34e22fcee1 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 a21b2804c9..c96fb46fc1 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 8836163caa..c102e1c238 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 f6abe2645c..b63658a0f0 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 286bdcda67..a39551d38d 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 4ded64904b..066e979c80 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