From 7f9ccd39f51ede6f0bed337cde7afe7db6569704 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Fri, 14 Jun 2024 11:54:37 -0400 Subject: [PATCH] LibJS+LibLocale: Replace relative time formatting with ICU This uses ICU for all of the Intl.RelativeTimeFormat prototypes, which lets us remove all data from our relative-time format generator. --- Meta/CMake/locale_data.cmake | 13 - .../CodeGenerators/LibLocale/CMakeLists.txt | 1 - .../GenerateRelativeTimeFormatData.cpp | 304 ------------------ .../LibJS/Runtime/Intl/RelativeTimeFormat.cpp | 208 ++---------- .../LibJS/Runtime/Intl/RelativeTimeFormat.h | 46 +-- .../Intl/RelativeTimeFormatConstructor.cpp | 153 ++++----- .../Intl/RelativeTimeFormatConstructor.h | 4 +- .../LibLocale/RelativeTimeFormat.cpp | 217 ++++++++++++- .../Libraries/LibLocale/RelativeTimeFormat.h | 38 ++- 9 files changed, 353 insertions(+), 631 deletions(-) delete mode 100644 Meta/Lagom/Tools/CodeGenerators/LibLocale/GenerateRelativeTimeFormatData.cpp diff --git a/Meta/CMake/locale_data.cmake b/Meta/CMake/locale_data.cmake index 41098e5df05..21a47b82b95 100644 --- a/Meta/CMake/locale_data.cmake +++ b/Meta/CMake/locale_data.cmake @@ -44,9 +44,6 @@ if (ENABLE_UNICODE_DATABASE_DOWNLOAD) set(PLURAL_RULES_DATA_HEADER PluralRulesData.h) set(PLURAL_RULES_DATA_IMPLEMENTATION PluralRulesData.cpp) - set(RELATIVE_TIME_FORMAT_DATA_HEADER RelativeTimeFormatData.h) - set(RELATIVE_TIME_FORMAT_DATA_IMPLEMENTATION RelativeTimeFormatData.cpp) - invoke_generator( "LocaleData" Lagom::GenerateLocaleData @@ -63,21 +60,11 @@ if (ENABLE_UNICODE_DATABASE_DOWNLOAD) "${PLURAL_RULES_DATA_IMPLEMENTATION}" arguments -r "${CLDR_CORE_PATH}" -l "${CLDR_LOCALES_PATH}" ) - invoke_generator( - "RelativeTimeFormatData" - Lagom::GenerateRelativeTimeFormatData - "${CLDR_VERSION_FILE}" - "${RELATIVE_TIME_FORMAT_DATA_HEADER}" - "${RELATIVE_TIME_FORMAT_DATA_IMPLEMENTATION}" - arguments -d "${CLDR_DATES_PATH}" - ) set(LOCALE_DATA_SOURCES ${LOCALE_DATA_HEADER} ${LOCALE_DATA_IMPLEMENTATION} ${PLURAL_RULES_DATA_HEADER} ${PLURAL_RULES_DATA_IMPLEMENTATION} - ${RELATIVE_TIME_FORMAT_DATA_HEADER} - ${RELATIVE_TIME_FORMAT_DATA_IMPLEMENTATION} ) endif() diff --git a/Meta/Lagom/Tools/CodeGenerators/LibLocale/CMakeLists.txt b/Meta/Lagom/Tools/CodeGenerators/LibLocale/CMakeLists.txt index ab084d567c4..f51f1ed909c 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibLocale/CMakeLists.txt +++ b/Meta/Lagom/Tools/CodeGenerators/LibLocale/CMakeLists.txt @@ -1,3 +1,2 @@ lagom_tool(GenerateLocaleData SOURCES GenerateLocaleData.cpp LIBS LibMain) lagom_tool(GeneratePluralRulesData SOURCES GeneratePluralRulesData.cpp LIBS LibMain) -lagom_tool(GenerateRelativeTimeFormatData SOURCES GenerateRelativeTimeFormatData.cpp LIBS LibMain) diff --git a/Meta/Lagom/Tools/CodeGenerators/LibLocale/GenerateRelativeTimeFormatData.cpp b/Meta/Lagom/Tools/CodeGenerators/LibLocale/GenerateRelativeTimeFormatData.cpp deleted file mode 100644 index 2fe54a6d201..00000000000 --- a/Meta/Lagom/Tools/CodeGenerators/LibLocale/GenerateRelativeTimeFormatData.cpp +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Copyright (c) 2022-2023, Tim Flynn - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include "../LibUnicode/GeneratorUtil.h" // FIXME: Move this somewhere common. -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -struct RelativeTimeFormat { - unsigned hash() const - { - auto hash = time_unit.hash(); - hash = pair_int_hash(hash, style.hash()); - hash = pair_int_hash(hash, plurality.hash()); - hash = pair_int_hash(hash, tense_or_number); - hash = pair_int_hash(hash, pattern); - return hash; - } - - bool operator==(RelativeTimeFormat const& other) const - { - return (time_unit == other.time_unit) - && (plurality == other.plurality) - && (style == other.style) - && (tense_or_number == other.tense_or_number) - && (pattern == other.pattern); - } - - ByteString time_unit; - ByteString style; - ByteString plurality; - size_t tense_or_number { 0 }; - size_t pattern { 0 }; -}; - -template<> -struct AK::Formatter : Formatter { - ErrorOr format(FormatBuilder& builder, RelativeTimeFormat const& format) - { - return Formatter::format(builder, - "{{ TimeUnit::{}, Style::{}, PluralCategory::{}, {}, {} }}"sv, - format.time_unit, - format.style, - format.plurality, - format.tense_or_number, - format.pattern); - } -}; - -template<> -struct AK::Traits : public DefaultTraits { - static unsigned hash(RelativeTimeFormat const& format) { return format.hash(); } -}; - -struct LocaleData { - Vector time_units; -}; - -struct CLDR { - UniqueStringStorage unique_strings; - UniqueStorage unique_formats; - - HashMap locales; -}; - -static ErrorOr parse_date_fields(ByteString locale_dates_path, CLDR& cldr, LocaleData& locale) -{ - LexicalPath date_fields_path(move(locale_dates_path)); - date_fields_path = date_fields_path.append("dateFields.json"sv); - - auto date_fields = TRY(read_json_file(date_fields_path.string())); - auto const& main_object = date_fields.as_object().get_object("main"sv).value(); - auto const& locale_object = main_object.get_object(date_fields_path.parent().basename()).value(); - auto const& dates_object = locale_object.get_object("dates"sv).value(); - auto const& fields_object = dates_object.get_object("fields"sv).value(); - - auto is_sanctioned_unit = [](auto unit) { - // This is a copy of the time units sanctioned for use within ECMA-402. - // https://tc39.es/ecma402/#sec-singularrelativetimeunit - return unit.is_one_of("second"sv, "minute"sv, "hour"sv, "day"sv, "week"sv, "month"sv, "quarter"sv, "year"sv); - }; - - auto parse_pattern = [&](auto unit, auto style, auto plurality, auto tense_or_number, auto const& pattern) { - RelativeTimeFormat format {}; - format.time_unit = unit.to_titlecase_string(); - format.style = style.to_titlecase_string(); - format.plurality = plurality.to_titlecase_string(); - format.tense_or_number = cldr.unique_strings.ensure(tense_or_number); - format.pattern = cldr.unique_strings.ensure(pattern.as_string()); - - locale.time_units.append(cldr.unique_formats.ensure(move(format))); - }; - - fields_object.for_each_member([&](auto const& unit_and_style, auto const& patterns) { - auto segments = unit_and_style.split_view('-'); - auto unit = segments[0]; - auto style = (segments.size() > 1) ? segments[1] : "long"sv; - - if (!is_sanctioned_unit(unit)) - return; - - patterns.as_object().for_each_member([&](auto const& type, auto const& pattern_value) { - constexpr auto number_key = "relative-type-"sv; - constexpr auto tense_key = "relativeTime-type-"sv; - constexpr auto plurality_key = "relativeTimePattern-count-"sv; - - if (type.starts_with(number_key)) { - auto number = type.substring_view(number_key.length()); - parse_pattern(unit, style, "Other"sv, number, pattern_value); - } else if (type.starts_with(tense_key)) { - pattern_value.as_object().for_each_member([&](auto const& key, auto const& pattern) { - VERIFY(key.starts_with(plurality_key)); - auto plurality = key.substring_view(plurality_key.length()); - auto tense = type.substring_view(tense_key.length()); - - parse_pattern(unit, style, plurality, tense, pattern); - }); - } - }); - }); - - return {}; -} - -static ErrorOr parse_all_locales(ByteString dates_path, CLDR& cldr) -{ - auto remove_variants_from_path = [&](ByteString path) -> ErrorOr { - auto parsed_locale = TRY(CanonicalLanguageID::parse(cldr.unique_strings, LexicalPath::basename(path))); - - StringBuilder builder; - builder.append(cldr.unique_strings.get(parsed_locale.language)); - if (auto script = cldr.unique_strings.get(parsed_locale.script); !script.is_empty()) - builder.appendff("-{}", script); - if (auto region = cldr.unique_strings.get(parsed_locale.region); !region.is_empty()) - builder.appendff("-{}", region); - - return builder.to_byte_string(); - }; - - TRY(Core::Directory::for_each_entry(TRY(String::formatted("{}/main", dates_path)), Core::DirIterator::SkipParentAndBaseDir, [&](auto& entry, auto& directory) -> ErrorOr { - auto dates_path = LexicalPath::join(directory.path().string(), entry.name).string(); - auto language = TRY(remove_variants_from_path(dates_path)); - - auto& locale = cldr.locales.ensure(language); - TRY(parse_date_fields(move(dates_path), cldr, locale)); - return IterationDecision::Continue; - })); - - return {}; -} - -static ErrorOr generate_unicode_locale_header(Core::InputBufferedFile& file, CLDR&) -{ - StringBuilder builder; - SourceGenerator generator { builder }; - - generator.append(R"~~~( -#pragma once - -#include - -namespace Locale { -)~~~"); - - generator.append(R"~~~( -} -)~~~"); - - TRY(file.write_until_depleted(generator.as_string_view().bytes())); - return {}; -} - -static ErrorOr generate_unicode_locale_implementation(Core::InputBufferedFile& file, CLDR& cldr) -{ - StringBuilder builder; - SourceGenerator generator { builder }; - generator.set("string_index_type"sv, cldr.unique_strings.type_that_fits()); - generator.set("relative_time_format_index_type"sv, cldr.unique_formats.type_that_fits()); - - generator.append(R"~~~( -#include -#include -#include -#include -#include -#include -#include - -namespace Locale { -)~~~"); - - cldr.unique_strings.generate(generator); - - generator.append(R"~~~( -struct RelativeTimeFormatImpl { - RelativeTimeFormat to_relative_time_format() const - { - RelativeTimeFormat relative_time_format {}; - relative_time_format.plurality = plurality; - relative_time_format.pattern = decode_string(pattern); - - return relative_time_format; - } - - TimeUnit time_unit; - Style style; - PluralCategory plurality; - @string_index_type@ tense_or_number { 0 }; - @string_index_type@ pattern { 0 }; -}; -)~~~"); - - cldr.unique_formats.generate(generator, "RelativeTimeFormatImpl"sv, "s_relative_time_formats"sv, 10); - - auto append_list = [&](ByteString name, auto const& list) { - generator.set("name", name); - generator.set("size", ByteString::number(list.size())); - - generator.append(R"~~~( -static constexpr Array<@relative_time_format_index_type@, @size@> @name@ { {)~~~"); - - bool first = true; - for (auto index : list) { - generator.append(first ? " "sv : ", "sv); - generator.append(ByteString::number(index)); - first = false; - } - - generator.append(" } };"); - }; - - generate_mapping(generator, cldr.locales, cldr.unique_formats.type_that_fits(), "s_locale_relative_time_formats"sv, "s_number_systems_digits_{}"sv, nullptr, [&](auto const& name, auto const& value) { append_list(name, value.time_units); }); - - generator.append(R"~~~( -Vector get_relative_time_format_patterns(StringView locale, TimeUnit time_unit, StringView tense_or_number, Style style) -{ - Vector formats; - - auto locale_value = locale_from_string(locale); - if (!locale_value.has_value()) - return formats; - - auto locale_index = to_underlying(*locale_value) - 1; // Subtract 1 because 0 == Locale::None. - auto const& locale_formats = s_locale_relative_time_formats.at(locale_index); - - for (auto const& locale_format_index : locale_formats) { - auto const& locale_format = s_relative_time_formats.at(locale_format_index); - - if (locale_format.time_unit != time_unit) - continue; - if (locale_format.style != style) - continue; - if (decode_string(locale_format.tense_or_number) != tense_or_number) - continue; - - formats.append(locale_format.to_relative_time_format()); - } - - return formats; -} - -} -)~~~"); - - TRY(file.write_until_depleted(generator.as_string_view().bytes())); - return {}; -} - -ErrorOr serenity_main(Main::Arguments arguments) -{ - StringView generated_header_path; - StringView generated_implementation_path; - StringView dates_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(dates_path, "Path to cldr-dates directory", "dates-path", 'd', "dates-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(dates_path, cldr)); - - TRY(generate_unicode_locale_header(*generated_header_file, cldr)); - TRY(generate_unicode_locale_implementation(*generated_implementation_file, cldr)); - - return 0; -} diff --git a/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.cpp index 1f752c04ac4..9fffc46dffb 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.cpp @@ -4,6 +4,7 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include @@ -23,223 +24,75 @@ RelativeTimeFormat::RelativeTimeFormat(Object& prototype) { } -void RelativeTimeFormat::visit_edges(Cell::Visitor& visitor) -{ - Base::visit_edges(visitor); - if (m_number_format) - visitor.visit(m_number_format); - if (m_plural_rules) - visitor.visit(m_plural_rules); -} - -void RelativeTimeFormat::set_numeric(StringView numeric) -{ - if (numeric == "always"sv) { - m_numeric = Numeric::Always; - } else if (numeric == "auto"sv) { - m_numeric = Numeric::Auto; - } else { - VERIFY_NOT_REACHED(); - } -} - -StringView RelativeTimeFormat::numeric_string() const -{ - switch (m_numeric) { - case Numeric::Always: - return "always"sv; - case Numeric::Auto: - return "auto"sv; - default: - VERIFY_NOT_REACHED(); - } -} - // 17.5.1 SingularRelativeTimeUnit ( unit ), https://tc39.es/ecma402/#sec-singularrelativetimeunit ThrowCompletionOr<::Locale::TimeUnit> singular_relative_time_unit(VM& vm, StringView unit) { - // 1. Assert: Type(unit) is String. - - // 2. If unit is "seconds", return "second". + // 1. If unit is "seconds", return "second". if (unit == "seconds"sv) return ::Locale::TimeUnit::Second; - // 3. If unit is "minutes", return "minute". + // 2. If unit is "minutes", return "minute". if (unit == "minutes"sv) return ::Locale::TimeUnit::Minute; - // 4. If unit is "hours", return "hour". + // 3. If unit is "hours", return "hour". if (unit == "hours"sv) return ::Locale::TimeUnit::Hour; - // 5. If unit is "days", return "day". + // 4. If unit is "days", return "day". if (unit == "days"sv) return ::Locale::TimeUnit::Day; - // 6. If unit is "weeks", return "week". + // 5. If unit is "weeks", return "week". if (unit == "weeks"sv) return ::Locale::TimeUnit::Week; - // 7. If unit is "months", return "month". + // 6. If unit is "months", return "month". if (unit == "months"sv) return ::Locale::TimeUnit::Month; - // 8. If unit is "quarters", return "quarter". + // 7. If unit is "quarters", return "quarter". if (unit == "quarters"sv) return ::Locale::TimeUnit::Quarter; - // 9. If unit is "years", return "year". + // 8. If unit is "years", return "year". if (unit == "years"sv) return ::Locale::TimeUnit::Year; - // 10. If unit is not one of "second", "minute", "hour", "day", "week", "month", "quarter", or "year", throw a RangeError exception. - // 11. Return unit. + // 9. If unit is not one of "second", "minute", "hour", "day", "week", "month", "quarter", or "year", throw a RangeError exception. + // 10. Return unit. if (auto time_unit = ::Locale::time_unit_from_string(unit); time_unit.has_value()) return *time_unit; return vm.throw_completion(ErrorType::IntlInvalidUnit, unit); } // 17.5.2 PartitionRelativeTimePattern ( relativeTimeFormat, value, unit ), https://tc39.es/ecma402/#sec-PartitionRelativeTimePattern -ThrowCompletionOr> partition_relative_time_pattern(VM& vm, RelativeTimeFormat& relative_time_format, double value, StringView unit) +ThrowCompletionOr> partition_relative_time_pattern(VM& vm, RelativeTimeFormat& relative_time_format, double value, StringView unit) { - // 1. Assert: relativeTimeFormat has an [[InitializedRelativeTimeFormat]] internal slot. - // 2. Assert: Type(value) is Number. - // 3. Assert: Type(unit) is String. - - // 4. If value is NaN, +∞𝔽, or -∞𝔽, throw a RangeError exception. + // 1. If value is NaN, +∞𝔽, or -∞𝔽, throw a RangeError exception. if (!Value(value).is_finite_number()) return vm.throw_completion(ErrorType::NumberIsNaNOrInfinity); - // 5. Let unit be ? SingularRelativeTimeUnit(unit). + // 2. Let unit be ? SingularRelativeTimeUnit(unit). auto time_unit = TRY(singular_relative_time_unit(vm, unit)); - // 6. Let localeData be %RelativeTimeFormat%.[[LocaleData]]. - // 7. Let dataLocale be relativeTimeFormat.[[DataLocale]]. - auto const& data_locale = relative_time_format.data_locale(); - - // 8. Let fields be localeData.[[]]. - - // 9. Let style be relativeTimeFormat.[[Style]]. - auto style = relative_time_format.style(); - - // NOTE: The next steps form a "key" based on combining various formatting options into a string, - // then filtering the large set of locale data down to the pattern we are looking for. Instead, - // LibUnicode expects the individual options as enumeration values, and returns the couple of - // patterns that match those options. - auto find_patterns_for_tense_or_number = [&](StringView tense_or_number) { - // 10. If style is equal to "short", then - // a. Let entry be the string-concatenation of unit and "-short". - // 11. Else if style is equal to "narrow", then - // a. Let entry be the string-concatenation of unit and "-narrow". - // 12. Else, - // a. Let entry be unit. - auto patterns = ::Locale::get_relative_time_format_patterns(data_locale, time_unit, tense_or_number, style); - - // 13. If fields doesn't have a field [[]], then - if (patterns.is_empty()) { - // a. Let entry be unit. - // NOTE: In the CLDR, the lack of "short" or "narrow" in the key implies "long". - patterns = ::Locale::get_relative_time_format_patterns(data_locale, time_unit, tense_or_number, ::Locale::Style::Long); - } - - // 14. Let patterns be fields.[[]]. - return patterns; - }; - - // 15. Let numeric be relativeTimeFormat.[[Numeric]]. - // 16. If numeric is equal to "auto", then - if (relative_time_format.numeric() == RelativeTimeFormat::Numeric::Auto) { - // a. Let valueString be ToString(value). - auto value_string = MUST(Value(value).to_string(vm)); - - // b. If patterns has a field [[]], then - if (auto patterns = find_patterns_for_tense_or_number(value_string); !patterns.is_empty()) { - VERIFY(patterns.size() == 1); - - // i. Let result be patterns.[[]]. - auto result = MUST(String::from_utf8(patterns[0].pattern)); - - // ii. Return a List containing the Record { [[Type]]: "literal", [[Value]]: result }. - return Vector { { "literal"sv, move(result) } }; - } - } - - // 17. If value is -0𝔽 or if value is less than 0, then - StringView tense; - if (Value(value).is_negative_zero() || (value < 0)) { - // a. Let tl be "past". - tense = "past"sv; - - // FIXME: The spec does not say to do this, but nothing makes sense after this with a negative value. - value = fabs(value); - } - // 18. Else, - else { - // a. Let tl be "future". - tense = "future"sv; - } - - // 19. Let po be patterns.[[]]. - auto patterns = find_patterns_for_tense_or_number(tense); - - // 20. Let fv be ! PartitionNumberPattern(relativeTimeFormat.[[NumberFormat]], 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)); - - // 22. Let pattern be po.[[]]. - auto pattern = patterns.find_if([&](auto& p) { return p.plurality == plurality.plural_category; }); - if (pattern == patterns.end()) - return Vector {}; - - // 23. Return ! MakePartsList(pattern, unit, fv). - return make_parts_list(pattern->pattern, ::Locale::time_unit_to_string(time_unit), move(value_partitions)); -} - -// 17.5.3 MakePartsList ( pattern, unit, parts ), https://tc39.es/ecma402/#sec-makepartslist -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); - - // 2. Let result be a new empty List. - Vector result; - - // 3. For each Record { [[Type]], [[Value]] } patternPart in patternParts, do - for (auto& pattern_part : pattern_parts) { - // a. If patternPart.[[Type]] is "literal", then - if (pattern_part.type == "literal"sv) { - // i. Append Record { [[Type]]: "literal", [[Value]]: patternPart.[[Value]], [[Unit]]: empty } to result. - result.empend("literal"sv, move(pattern_part.value)); - } - // b. Else, - else { - // i. Assert: patternPart.[[Type]] is "0". - VERIFY(pattern_part.type == "0"sv); - - // ii. For each Record { [[Type]], [[Value]] } part in parts, do - for (auto& part : parts) { - // 1. Append Record { [[Type]]: part.[[Type]], [[Value]]: part.[[Value]], [[Unit]]: unit } to result. - result.empend(part.type, move(part.value), unit); - } - } - } - - // 4. Return result. - return result; + return relative_time_format.formatter().format_to_parts(value, time_unit, relative_time_format.numeric()); } // 17.5.4 FormatRelativeTime ( relativeTimeFormat, value, unit ), https://tc39.es/ecma402/#sec-FormatRelativeTime ThrowCompletionOr format_relative_time(VM& vm, RelativeTimeFormat& relative_time_format, double value, StringView unit) { // 1. Let parts be ? PartitionRelativeTimePattern(relativeTimeFormat, value, unit). - auto parts = TRY(partition_relative_time_pattern(vm, relative_time_format, value, unit)); + auto time_unit = TRY([&]() -> ThrowCompletionOr<::Locale::TimeUnit> { + // NOTE: We short-circuit PartitionRelativeTimePattern as we do not need individual partitions. But we must still + // perform the NaN/Infinity sanity checks and unit parsing from its first steps. + + // 1. If value is NaN, +∞𝔽, or -∞𝔽, throw a RangeError exception. + if (!Value(value).is_finite_number()) + return vm.throw_completion(ErrorType::NumberIsNaNOrInfinity); + + // 2. Let unit be ? SingularRelativeTimeUnit(unit). + return TRY(singular_relative_time_unit(vm, unit)); + }()); // 2. Let result be an empty String. - StringBuilder result; - // 3. For each Record { [[Type]], [[Value]], [[Unit]] } 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 relative_time_format.formatter().format(value, time_unit, relative_time_format.numeric()); } // 17.5.5 FormatRelativeTimeToParts ( relativeTimeFormat, value, unit ), https://tc39.es/ecma402/#sec-FormatRelativeTimeToParts @@ -254,10 +107,8 @@ ThrowCompletionOr> format_relative_time_to_parts(VM& vm, Rel auto result = MUST(Array::create(realm, 0)); // 3. Let n be 0. - size_t n = 0; - // 4. For each Record { [[Type]], [[Value]], [[Unit]] } part in parts, do - for (auto& part : parts) { + for (auto [n, part] : enumerate(parts)) { // a. Let O be OrdinaryObjectCreate(%Object.prototype%). auto object = Object::create(realm, realm.intrinsics().object_prototype()); @@ -277,7 +128,6 @@ ThrowCompletionOr> format_relative_time_to_parts(VM& vm, Rel MUST(result->create_data_property_or_throw(n, object)); // f. Increment n by 1. - ++n; } // 5. Return result. diff --git a/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.h b/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.h index be07b0e05f5..54e4518162f 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormat.h @@ -23,11 +23,6 @@ class RelativeTimeFormat final : public Object { JS_DECLARE_ALLOCATOR(RelativeTimeFormat); public: - enum class Numeric { - Always, - Auto, - }; - static constexpr auto relevant_extension_keys() { // 17.2.3 Internal slots, https://tc39.es/ecma402/#sec-Intl.RelativeTimeFormat-internal-slots @@ -50,43 +45,28 @@ public: void set_style(StringView style) { m_style = ::Locale::style_from_string(style); } StringView style_string() const { return ::Locale::style_to_string(m_style); } - Numeric numeric() const { return m_numeric; } - void set_numeric(StringView numeric); - StringView numeric_string() const; + ::Locale::NumericDisplay numeric() const { return m_numeric; } + void set_numeric(StringView numeric) { m_numeric = ::Locale::numeric_display_from_string(numeric); } + StringView numeric_string() const { return ::Locale::numeric_display_to_string(m_numeric); } - NumberFormat& number_format() const { return *m_number_format; } - void set_number_format(NumberFormat* number_format) { m_number_format = number_format; } - - PluralRules& plural_rules() const { return *m_plural_rules; } - void set_plural_rules(PluralRules* plural_rules) { m_plural_rules = plural_rules; } + ::Locale::RelativeTimeFormat const& formatter() const { return *m_formatter; } + void set_formatter(NonnullOwnPtr<::Locale::RelativeTimeFormat> formatter) { m_formatter = move(formatter); } private: explicit RelativeTimeFormat(Object& prototype); - virtual void visit_edges(Cell::Visitor&) override; + String m_locale; // [[Locale]] + String m_data_locale; // [[DataLocale]] + String m_numbering_system; // [[NumberingSystem]] + ::Locale::Style m_style { ::Locale::Style::Long }; // [[Style]] + ::Locale::NumericDisplay m_numeric { ::Locale::NumericDisplay::Always }; // [[Numeric]] - String m_locale; // [[Locale]] - String m_data_locale; // [[DataLocale]] - String m_numbering_system; // [[NumberingSystem]] - ::Locale::Style m_style { ::Locale::Style::Long }; // [[Style]] - Numeric m_numeric { Numeric::Always }; // [[Numeric]] - GCPtr m_number_format; // [[NumberFormat]] - GCPtr m_plural_rules; // [[PluralRules]] -}; - -struct PatternPartitionWithUnit : public PatternPartition { - PatternPartitionWithUnit(StringView type, String value, StringView unit_string = {}) - : PatternPartition(type, move(value)) - , unit(unit_string) - { - } - - StringView unit; + // Non-standard. Stores the ICU relative-time formatter for the Intl object's formatting options. + OwnPtr<::Locale::RelativeTimeFormat> m_formatter; }; 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<::Locale::NumberFormat::Partition> parts); +ThrowCompletionOr> partition_relative_time_pattern(VM&, RelativeTimeFormat&, double value, StringView unit); 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/Intl/RelativeTimeFormatConstructor.cpp b/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormatConstructor.cpp index 8a0a2a269d0..7bb234761b4 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormatConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormatConstructor.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,10 +8,6 @@ #include #include #include -#include -#include -#include -#include #include #include #include @@ -52,14 +48,77 @@ ThrowCompletionOr> RelativeTimeFormatConstructor::construct { auto& vm = this->vm(); - auto locales = vm.argument(0); - auto options = vm.argument(1); + auto locales_value = vm.argument(0); + auto options_value = vm.argument(1); - // 2. Let relativeTimeFormat be ? OrdinaryCreateFromConstructor(NewTarget, "%RelativeTimeFormat.prototype%", « [[InitializedRelativeTimeFormat]], [[Locale]], [[DataLocale]], [[Style]], [[Numeric]], [[NumberFormat]], [[NumberingSystem]], [[PluralRules]] »). + // 2. Let relativeTimeFormat be ? OrdinaryCreateFromConstructor(NewTarget, "%RelativeTimeFormat.prototype%", « [[InitializedRelativeTimeFormat]], [[Locale]], [[LocaleData]], [[Style]], [[Numeric]], [[NumberFormat]], [[NumberingSystem]], [[PluralRules]] »). auto relative_time_format = TRY(ordinary_create_from_constructor(vm, new_target, &Intrinsics::intl_relative_time_format_prototype)); - // 3. Return ? InitializeRelativeTimeFormat(relativeTimeFormat, locales, options). - return TRY(initialize_relative_time_format(vm, relative_time_format, locales, options)); + // 3. Let requestedLocales be ? CanonicalizeLocaleList(locales). + auto requested_locales = TRY(canonicalize_locale_list(vm, locales_value)); + + // 4. Set options to ? CoerceOptionsToObject(options). + auto* options = TRY(coerce_options_to_object(vm, options_value)); + + // 5. Let opt be a new Record. + LocaleOptions opt {}; + + // 6. Let matcher be ? GetOption(options, "localeMatcher", string, « "lookup", "best fit" », "best fit"). + auto matcher = TRY(get_option(vm, *options, vm.names.localeMatcher, OptionType::String, AK::Array { "lookup"sv, "best fit"sv }, "best fit"sv)); + + // 7. Set opt.[[LocaleMatcher]] to matcher. + opt.locale_matcher = matcher; + + // 8. Let numberingSystem be ? GetOption(options, "numberingSystem", string, empty, undefined). + auto numbering_system = TRY(get_option(vm, *options, vm.names.numberingSystem, OptionType::String, {}, Empty {})); + + // 9. If numberingSystem is not undefined, then + if (!numbering_system.is_undefined()) { + // a. If numberingSystem cannot be matched by the type Unicode locale nonterminal, throw a RangeError exception. + if (!::Locale::is_type_identifier(numbering_system.as_string().utf8_string_view())) + return vm.throw_completion(ErrorType::OptionIsNotValidValue, numbering_system, "numberingSystem"sv); + + // 10. Set opt.[[nu]] to numberingSystem. + opt.nu = numbering_system.as_string().utf8_string(); + } + + // 11. Let r be ResolveLocale(%Intl.RelativeTimeFormat%.[[AvailableLocales]], requestedLocales, opt, %Intl.RelativeTimeFormat%.[[RelevantExtensionKeys]], %Intl.RelativeTimeFormat%.[[LocaleData]]). + auto result = resolve_locale(requested_locales, opt, RelativeTimeFormat::relevant_extension_keys()); + + // 12. Let locale be r.[[Locale]]. + auto locale = move(result.locale); + + // 13. Set relativeTimeFormat.[[Locale]] to locale. + relative_time_format->set_locale(locale); + + // 14. Set relativeTimeFormat.[[LocaleData]] to r.[[LocaleData]]. + relative_time_format->set_data_locale(move(result.data_locale)); + + // 15. Set relativeTimeFormat.[[NumberingSystem]] to r.[[nu]]. + if (result.nu.has_value()) + relative_time_format->set_numbering_system(result.nu.release_value()); + + // 16. Let style be ? GetOption(options, "style", string, « "long", "short", "narrow" », "long"). + auto style = TRY(get_option(vm, *options, vm.names.style, OptionType::String, { "long"sv, "short"sv, "narrow"sv }, "long"sv)); + + // 17. Set relativeTimeFormat.[[Style]] to style. + relative_time_format->set_style(style.as_string().utf8_string_view()); + + // 18. Let numeric be ? GetOption(options, "numeric", string, « "always", "auto" », "always"). + auto numeric = TRY(get_option(vm, *options, vm.names.numeric, OptionType::String, { "always"sv, "auto"sv }, "always"sv)); + + // 19. Set relativeTimeFormat.[[Numeric]] to numeric. + relative_time_format->set_numeric(numeric.as_string().utf8_string_view()); + + // 20. Let relativeTimeFormat.[[NumberFormat]] be ! Construct(%Intl.NumberFormat%, « locale »). + // 21. Let relativeTimeFormat.[[PluralRules]] be ! Construct(%Intl.PluralRules%, « locale »). + auto formatter = ::Locale::RelativeTimeFormat::create( + relative_time_format->locale(), + relative_time_format->style()); + relative_time_format->set_formatter(move(formatter)); + + // 22. Return relativeTimeFormat. + return relative_time_format; } // 17.2.2 Intl.RelativeTimeFormat.supportedLocalesOf ( locales [ , options ] ), https://tc39.es/ecma402/#sec-Intl.RelativeTimeFormat.supportedLocalesOf @@ -77,78 +136,4 @@ JS_DEFINE_NATIVE_FUNCTION(RelativeTimeFormatConstructor::supported_locales_of) return TRY(supported_locales(vm, requested_locales, options)); } -// 17.1.2 InitializeRelativeTimeFormat ( relativeTimeFormat, locales, options ), https://tc39.es/ecma402/#sec-InitializeRelativeTimeFormat -ThrowCompletionOr> initialize_relative_time_format(VM& vm, RelativeTimeFormat& relative_time_format, Value locales_value, Value options_value) -{ - auto& realm = *vm.current_realm(); - - // 1. Let requestedLocales be ? CanonicalizeLocaleList(locales). - auto requested_locales = TRY(canonicalize_locale_list(vm, locales_value)); - - // 2. Set options to ? CoerceOptionsToObject(options). - auto* options = TRY(coerce_options_to_object(vm, options_value)); - - // 3. Let opt be a new Record. - LocaleOptions opt {}; - - // 4. Let matcher be ? GetOption(options, "localeMatcher", string, « "lookup", "best fit" », "best fit"). - auto matcher = TRY(get_option(vm, *options, vm.names.localeMatcher, OptionType::String, AK::Array { "lookup"sv, "best fit"sv }, "best fit"sv)); - - // 5. Set opt.[[LocaleMatcher]] to matcher. - opt.locale_matcher = matcher; - - // 6. Let numberingSystem be ? GetOption(options, "numberingSystem", string, empty, undefined). - auto numbering_system = TRY(get_option(vm, *options, vm.names.numberingSystem, OptionType::String, {}, Empty {})); - - // 7. If numberingSystem is not undefined, then - if (!numbering_system.is_undefined()) { - // a. If numberingSystem does not match the Unicode Locale Identifier type nonterminal, throw a RangeError exception. - if (!::Locale::is_type_identifier(numbering_system.as_string().utf8_string_view())) - return vm.throw_completion(ErrorType::OptionIsNotValidValue, numbering_system, "numberingSystem"sv); - - // 8. Set opt.[[nu]] to numberingSystem. - opt.nu = numbering_system.as_string().utf8_string(); - } - - // 9. Let localeData be %RelativeTimeFormat%.[[LocaleData]]. - // 10. Let r be ResolveLocale(%RelativeTimeFormat%.[[AvailableLocales]], requestedLocales, opt, %RelativeTimeFormat%.[[RelevantExtensionKeys]], localeData). - auto result = resolve_locale(requested_locales, opt, RelativeTimeFormat::relevant_extension_keys()); - - // 11. Let locale be r.[[locale]]. - auto locale = move(result.locale); - - // 12. Set relativeTimeFormat.[[Locale]] to locale. - relative_time_format.set_locale(locale); - - // 13. Set relativeTimeFormat.[[DataLocale]] to r.[[dataLocale]]. - relative_time_format.set_data_locale(move(result.data_locale)); - - // 14. Set relativeTimeFormat.[[NumberingSystem]] to r.[[nu]]. - if (result.nu.has_value()) - relative_time_format.set_numbering_system(result.nu.release_value()); - - // 15. Let style be ? GetOption(options, "style", string, « "long", "short", "narrow" », "long"). - auto style = TRY(get_option(vm, *options, vm.names.style, OptionType::String, { "long"sv, "short"sv, "narrow"sv }, "long"sv)); - - // 16. Set relativeTimeFormat.[[Style]] to style. - relative_time_format.set_style(style.as_string().utf8_string_view()); - - // 17. Let numeric be ? GetOption(options, "numeric", string, « "always", "auto" », "always"). - auto numeric = TRY(get_option(vm, *options, vm.names.numeric, OptionType::String, { "always"sv, "auto"sv }, "always"sv)); - - // 18. Set relativeTimeFormat.[[Numeric]] to numeric. - relative_time_format.set_numeric(numeric.as_string().utf8_string_view()); - - // 19. Let relativeTimeFormat.[[NumberFormat]] be ! Construct(%NumberFormat%, « locale »). - auto number_format = MUST(construct(vm, realm.intrinsics().intl_number_format_constructor(), PrimitiveString::create(vm, locale))); - relative_time_format.set_number_format(static_cast(number_format.ptr())); - - // 20. Let relativeTimeFormat.[[PluralRules]] be ! Construct(%PluralRules%, « locale »). - auto plural_rules = MUST(construct(vm, realm.intrinsics().intl_plural_rules_constructor(), PrimitiveString::create(vm, locale))); - relative_time_format.set_plural_rules(static_cast(plural_rules.ptr())); - - // 21. Return relativeTimeFormat. - return relative_time_format; -} - } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormatConstructor.h b/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormatConstructor.h index 305971c8a8c..7fb063e7443 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormatConstructor.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/RelativeTimeFormatConstructor.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -29,6 +29,4 @@ private: JS_DECLARE_NATIVE_FUNCTION(supported_locales_of); }; -ThrowCompletionOr> initialize_relative_time_format(VM& vm, RelativeTimeFormat& relative_time_format, Value locales_value, Value options_value); - } diff --git a/Userland/Libraries/LibLocale/RelativeTimeFormat.cpp b/Userland/Libraries/LibLocale/RelativeTimeFormat.cpp index 8fd447a24d6..9fe0a5eb9e9 100644 --- a/Userland/Libraries/LibLocale/RelativeTimeFormat.cpp +++ b/Userland/Libraries/LibLocale/RelativeTimeFormat.cpp @@ -1,11 +1,20 @@ /* - * Copyright (c) 2022-2023, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ +#define AK_DONT_REPLACE_STD + +#include +#include +#include #include +#include +#include +#include + namespace Locale { Optional time_unit_from_string(StringView time_unit) @@ -48,11 +57,211 @@ StringView time_unit_to_string(TimeUnit time_unit) return "quarter"sv; case TimeUnit::Year: return "year"sv; - default: - VERIFY_NOT_REACHED(); } + VERIFY_NOT_REACHED(); } -Vector __attribute__((weak)) get_relative_time_format_patterns(StringView, TimeUnit, StringView, Style) { return {}; } +static constexpr URelativeDateTimeUnit icu_time_unit(TimeUnit unit) +{ + switch (unit) { + case TimeUnit::Second: + return URelativeDateTimeUnit::UDAT_REL_UNIT_SECOND; + case TimeUnit::Minute: + return URelativeDateTimeUnit::UDAT_REL_UNIT_MINUTE; + case TimeUnit::Hour: + return URelativeDateTimeUnit::UDAT_REL_UNIT_HOUR; + case TimeUnit::Day: + return URelativeDateTimeUnit::UDAT_REL_UNIT_DAY; + case TimeUnit::Week: + return URelativeDateTimeUnit::UDAT_REL_UNIT_WEEK; + case TimeUnit::Month: + return URelativeDateTimeUnit::UDAT_REL_UNIT_MONTH; + case TimeUnit::Quarter: + return URelativeDateTimeUnit::UDAT_REL_UNIT_QUARTER; + case TimeUnit::Year: + return URelativeDateTimeUnit::UDAT_REL_UNIT_YEAR; + } + VERIFY_NOT_REACHED(); +} + +NumericDisplay numeric_display_from_string(StringView numeric_display) +{ + if (numeric_display == "always"sv) + return NumericDisplay::Always; + if (numeric_display == "auto"sv) + return NumericDisplay::Auto; + VERIFY_NOT_REACHED(); +} + +StringView numeric_display_to_string(NumericDisplay numeric_display) +{ + switch (numeric_display) { + case NumericDisplay::Always: + return "always"sv; + case NumericDisplay::Auto: + return "auto"sv; + } + VERIFY_NOT_REACHED(); +} + +static constexpr UDateRelativeDateTimeFormatterStyle icu_relative_date_time_style(Style unit_display) +{ + switch (unit_display) { + case Style::Long: + return UDAT_STYLE_LONG; + case Style::Short: + return UDAT_STYLE_SHORT; + case Style::Narrow: + return UDAT_STYLE_NARROW; + } + VERIFY_NOT_REACHED(); +} + +// 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_relative_time_format_field_to_string(i32 field) +{ + switch (field) { + case LITERAL_FIELD: + return "literal"sv; + case UNUM_INTEGER_FIELD: + return "integer"sv; + case UNUM_FRACTION_FIELD: + return "fraction"sv; + case UNUM_DECIMAL_SEPARATOR_FIELD: + return "decimal"sv; + case UNUM_GROUPING_SEPARATOR_FIELD: + return "group"sv; + } + VERIFY_NOT_REACHED(); +} + +struct Range { + i32 field { 0 }; + i32 start { 0 }; + i32 end { 0 }; +}; + +class RelativeTimeFormatImpl : public RelativeTimeFormat { +public: + explicit RelativeTimeFormatImpl(NonnullOwnPtr formatter) + : m_formatter(move(formatter)) + { + } + + virtual ~RelativeTimeFormatImpl() override = default; + + virtual String format(double time, TimeUnit unit, NumericDisplay numeric_display) const override + { + UErrorCode status = U_ZERO_ERROR; + + auto formatted = format_impl(time, unit, numeric_display); + + auto formatted_time = formatted->toTempString(status); + if (icu_failure(status)) + return {}; + + return icu_string_to_string(formatted_time); + } + + virtual Vector format_to_parts(double time, TimeUnit unit, NumericDisplay numeric_display) const override + { + UErrorCode status = U_ZERO_ERROR; + + auto formatted = format_impl(time, unit, numeric_display); + auto unit_string = time_unit_to_string(unit); + + auto formatted_time = formatted->toTempString(status); + if (icu_failure(status)) + return {}; + + Vector result; + Vector separators; + + auto create_partition = [&](i32 field, i32 begin, i32 end, bool is_unit) { + Partition partition; + partition.type = icu_relative_time_format_field_to_string(field); + partition.value = icu_string_to_string(formatted_time.tempSubStringBetween(begin, end)); + if (is_unit) + partition.unit = unit_string; + result.append(move(partition)); + }; + + icu::ConstrainedFieldPosition position; + position.constrainCategory(UFIELD_CATEGORY_NUMBER); + + i32 previous_end_index = 0; + + while (static_cast(formatted->nextPosition(position, status)) && icu_success(status)) { + if (position.getField() == UNUM_GROUPING_SEPARATOR_FIELD) { + separators.empend(position.getField(), position.getStart(), position.getLimit()); + continue; + } + + if (previous_end_index < position.getStart()) + create_partition(LITERAL_FIELD, previous_end_index, position.getStart(), false); + + auto start = position.getStart(); + + if (position.getField() == UNUM_INTEGER_FIELD) { + for (auto const& separator : separators) { + if (start >= separator.start) + continue; + + create_partition(position.getField(), start, separator.start, true); + create_partition(separator.field, separator.start, separator.end, true); + + start = separator.end; + break; + } + } + + create_partition(position.getField(), start, position.getLimit(), true); + previous_end_index = position.getLimit(); + } + + if (previous_end_index < formatted_time.length()) + create_partition(LITERAL_FIELD, previous_end_index, formatted_time.length(), false); + + return result; + } + +private: + Optional format_impl(double time, TimeUnit unit, NumericDisplay numeric_display) const + { + UErrorCode status = U_ZERO_ERROR; + + auto formatted = numeric_display == NumericDisplay::Always + ? m_formatter->formatNumericToValue(time, icu_time_unit(unit), status) + : m_formatter->formatToValue(time, icu_time_unit(unit), status); + if (icu_failure(status)) + return {}; + + return formatted; + } + + NonnullOwnPtr m_formatter; +}; + +NonnullOwnPtr RelativeTimeFormat::create(StringView locale, Style style) +{ + UErrorCode status = U_ZERO_ERROR; + + auto locale_data = LocaleData::for_locale(locale); + VERIFY(locale_data.has_value()); + + auto* number_formatter = icu::NumberFormat::createInstance(locale_data->locale(), UNUM_DECIMAL, status); + VERIFY(locale_data.has_value()); + + if (number_formatter->getDynamicClassID() == icu::DecimalFormat::getStaticClassID()) + static_cast(*number_formatter).setMinimumGroupingDigits(UNUM_MINIMUM_GROUPING_DIGITS_AUTO); + + auto formatter = make(locale_data->locale(), number_formatter, icu_relative_date_time_style(style), UDISPCTX_CAPITALIZATION_NONE, status); + VERIFY(icu_success(status)); + + return make(move(formatter)); +} } diff --git a/Userland/Libraries/LibLocale/RelativeTimeFormat.h b/Userland/Libraries/LibLocale/RelativeTimeFormat.h index 0d16a57db72..7019d18bba5 100644 --- a/Userland/Libraries/LibLocale/RelativeTimeFormat.h +++ b/Userland/Libraries/LibLocale/RelativeTimeFormat.h @@ -1,16 +1,17 @@ /* - * Copyright (c) 2022-2023, Tim Flynn + * Copyright (c) 2022-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ #pragma once +#include #include +#include #include #include #include -#include namespace Locale { @@ -25,15 +26,32 @@ enum class TimeUnit { Quarter, Year, }; +Optional time_unit_from_string(StringView); +StringView time_unit_to_string(TimeUnit); -struct RelativeTimeFormat { - PluralCategory plurality; - StringView pattern; +enum class NumericDisplay { + Always, + Auto, +}; +NumericDisplay numeric_display_from_string(StringView); +StringView numeric_display_to_string(NumericDisplay); + +class RelativeTimeFormat { +public: + static NonnullOwnPtr create(StringView locale, Style style); + virtual ~RelativeTimeFormat() = default; + + struct Partition { + StringView type; + String value; + StringView unit; + }; + + virtual String format(double, TimeUnit, NumericDisplay) const = 0; + virtual Vector format_to_parts(double, TimeUnit, NumericDisplay) const = 0; + +protected: + RelativeTimeFormat() = default; }; -Optional time_unit_from_string(StringView time_unit); -StringView time_unit_to_string(TimeUnit time_unit); - -Vector get_relative_time_format_patterns(StringView locale, TimeUnit time_unit, StringView tense_or_number, Style style); - }