From 273694d8de9365f6d291148fc8da73daed481f44 Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Wed, 12 Jun 2024 10:47:20 -0400 Subject: [PATCH] LibJS+LibLocale: Replace date-time formatting with ICU This uses ICU for the Intl.DateTimeFormat `format` `formatToParts`, `formatRange`, and `formatRangeToParts`. This lets us remove most data from our date-time format generator. All that remains are time zone data and locale week info, which are relied upon still for other interfaces. So they will be removed in a future patch. Note: All of the changes to the test files in this patch are now aligned with other browsers. This includes: * Some very incorrect formatting of Japanese symbols. (Looking at the old results now, it's very obvious they were wrong.) * Old FIXMEs regarding range formatting not including the start/end date when only time fields were requested, but the dates differ. * Day period inconsistencies. --- .../LibLocale/GenerateDateTimeFormatData.cpp | 1671 +---------------- Tests/LibLocale/CMakeLists.txt | 3 - Tests/LibLocale/TestDateTimeFormat.cpp | 188 -- Userland/Libraries/LibJS/Print.cpp | 6 +- .../LibJS/Runtime/Intl/DateTimeFormat.cpp | 1078 +---------- .../LibJS/Runtime/Intl/DateTimeFormat.h | 135 +- .../Intl/DateTimeFormatConstructor.cpp | 108 +- .../Runtime/Intl/DateTimeFormatPrototype.cpp | 6 +- .../DateTimeFormat.prototype.format.js | 86 +- .../DateTimeFormat.prototype.formatRange.js | 38 +- ...TimeFormat.prototype.formatRangeToParts.js | 151 +- ...ateTimeFormat.prototype.resolvedOptions.js | 4 +- .../Libraries/LibLocale/DateTimeFormat.cpp | 910 +++++++-- Userland/Libraries/LibLocale/DateTimeFormat.h | 210 +-- Userland/Libraries/LibLocale/Forward.h | 7 - Userland/Libraries/LibLocale/ICU.cpp | 5 + Userland/Libraries/LibLocale/ICU.h | 1 + 17 files changed, 1139 insertions(+), 3468 deletions(-) delete mode 100644 Tests/LibLocale/TestDateTimeFormat.cpp diff --git a/Meta/Lagom/Tools/CodeGenerators/LibLocale/GenerateDateTimeFormatData.cpp b/Meta/Lagom/Tools/CodeGenerators/LibLocale/GenerateDateTimeFormatData.cpp index 81d39662fb2..0cb179c1aea 100644 --- a/Meta/Lagom/Tools/CodeGenerators/LibLocale/GenerateDateTimeFormatData.cpp +++ b/Meta/Lagom/Tools/CodeGenerators/LibLocale/GenerateDateTimeFormatData.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -11,7 +11,6 @@ #include #include #include -#include #include #include #include @@ -27,312 +26,6 @@ #include #include -struct CalendarPattern : public Locale::CalendarPattern { - bool contains_only_date_fields() const - { - return !day_period.has_value() && !hour.has_value() && !minute.has_value() && !second.has_value() && !fractional_second_digits.has_value() && !time_zone_name.has_value(); - } - - bool contains_only_time_fields() const - { - return !weekday.has_value() && !era.has_value() && !year.has_value() && !month.has_value() && !day.has_value(); - } - - unsigned hash() const - { - auto hash = pair_int_hash(pattern_index, pattern12_index); - hash = pair_int_hash(hash, skeleton_index); - - auto hash_field = [&](auto const& field) { - if (field.has_value()) - hash = pair_int_hash(hash, static_cast(*field)); - else - hash = pair_int_hash(hash, -1); - }; - - hash_field(era); - hash_field(year); - hash_field(month); - hash_field(weekday); - hash_field(day); - hash_field(day_period); - hash_field(hour); - hash_field(minute); - hash_field(second); - hash_field(fractional_second_digits); - hash_field(time_zone_name); - - return hash; - } - - bool operator==(CalendarPattern const& other) const - { - return (skeleton_index == other.skeleton_index) - && (pattern_index == other.pattern_index) - && (pattern12_index == other.pattern12_index) - && (era == other.era) - && (year == other.year) - && (month == other.month) - && (weekday == other.weekday) - && (day == other.day) - && (day_period == other.day_period) - && (hour == other.hour) - && (minute == other.minute) - && (second == other.second) - && (fractional_second_digits == other.fractional_second_digits) - && (time_zone_name == other.time_zone_name); - } - - size_t skeleton_index { 0 }; - size_t pattern_index { 0 }; - size_t pattern12_index { 0 }; -}; - -template<> -struct AK::Formatter : Formatter { - ErrorOr format(FormatBuilder& builder, CalendarPattern const& pattern) - { - auto field_to_i8 = [](auto const& field) -> i8 { - if (!field.has_value()) - return -1; - return static_cast(*field); - }; - - return Formatter::format(builder, - "{{ {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} }}"sv, - pattern.skeleton_index, - pattern.pattern_index, - pattern.pattern12_index, - field_to_i8(pattern.era), - field_to_i8(pattern.year), - field_to_i8(pattern.month), - field_to_i8(pattern.weekday), - field_to_i8(pattern.day), - field_to_i8(pattern.day_period), - field_to_i8(pattern.hour), - field_to_i8(pattern.minute), - field_to_i8(pattern.second), - field_to_i8(pattern.fractional_second_digits), - field_to_i8(pattern.time_zone_name)); - } -}; - -template<> -struct AK::Traits : public DefaultTraits { - static unsigned hash(CalendarPattern const& c) { return c.hash(); } -}; - -struct CalendarRangePattern : public CalendarPattern { - unsigned hash() const - { - auto hash = CalendarPattern::hash(); - - if (field.has_value()) - hash = pair_int_hash(hash, static_cast(*field)); - hash = pair_int_hash(hash, start_range); - hash = pair_int_hash(hash, separator); - hash = pair_int_hash(hash, end_range); - - return hash; - } - - bool operator==(CalendarRangePattern const& other) const - { - if (!CalendarPattern::operator==(other)) - return false; - - return (field == other.field) - && (start_range == other.start_range) - && (separator == other.separator) - && (end_range == other.end_range); - } - - Optional field {}; - size_t start_range { 0 }; - size_t separator { 0 }; - size_t end_range { 0 }; -}; - -template<> -struct AK::Formatter : Formatter { - ErrorOr format(FormatBuilder& builder, CalendarRangePattern const& pattern) - { - auto field_to_i8 = [](auto const& field) -> i8 { - if (!field.has_value()) - return -1; - return static_cast(*field); - }; - - return Formatter::format(builder, - "{{ {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} }}"sv, - pattern.skeleton_index, - field_to_i8(pattern.field), - pattern.start_range, - pattern.separator, - pattern.end_range, - field_to_i8(pattern.era), - field_to_i8(pattern.year), - field_to_i8(pattern.month), - field_to_i8(pattern.weekday), - field_to_i8(pattern.day), - field_to_i8(pattern.day_period), - field_to_i8(pattern.hour), - field_to_i8(pattern.minute), - field_to_i8(pattern.second), - field_to_i8(pattern.fractional_second_digits), - field_to_i8(pattern.time_zone_name)); - } -}; - -template<> -struct AK::Traits : public DefaultTraits { - static unsigned hash(CalendarRangePattern const& c) { return c.hash(); } -}; - -struct CalendarFormat { - unsigned hash() const - { - auto hash = pair_int_hash(full_format, long_format); - hash = pair_int_hash(hash, medium_format); - hash = pair_int_hash(hash, short_format); - return hash; - } - - bool operator==(CalendarFormat const& other) const - { - return (full_format == other.full_format) - && (long_format == other.long_format) - && (medium_format == other.medium_format) - && (short_format == other.short_format); - } - - size_t full_format { 0 }; - size_t long_format { 0 }; - size_t medium_format { 0 }; - size_t short_format { 0 }; -}; - -template<> -struct AK::Formatter : Formatter { - ErrorOr format(FormatBuilder& builder, CalendarFormat const& pattern) - { - return Formatter::format(builder, - "{{ {}, {}, {}, {} }}"sv, - pattern.full_format, - pattern.long_format, - pattern.medium_format, - pattern.short_format); - } -}; - -template<> -struct AK::Traits : public DefaultTraits { - static unsigned hash(CalendarFormat const& c) { return c.hash(); } -}; - -using SymbolList = Vector; - -struct CalendarSymbols { - unsigned hash() const - { - auto hash = pair_int_hash(narrow_symbols, short_symbols); - hash = pair_int_hash(hash, long_symbols); - return hash; - } - - bool operator==(CalendarSymbols const& other) const - { - return (narrow_symbols == other.narrow_symbols) - && (short_symbols == other.short_symbols) - && (long_symbols == other.long_symbols); - } - - size_t narrow_symbols { 0 }; - size_t short_symbols { 0 }; - size_t long_symbols { 0 }; -}; - -template<> -struct AK::Formatter : Formatter { - ErrorOr format(FormatBuilder& builder, CalendarSymbols const& symbols) - { - return Formatter::format(builder, - "{{ {}, {}, {} }}"sv, - symbols.narrow_symbols, - symbols.short_symbols, - symbols.long_symbols); - } -}; - -template<> -struct AK::Traits : public DefaultTraits { - static unsigned hash(CalendarSymbols const& c) { return c.hash(); } -}; - -using CalendarPatternList = Vector; -using CalendarRangePatternList = Vector; -using CalendarSymbolsList = Vector; - -struct Calendar { - unsigned hash() const - { - auto hash = int_hash(date_formats); - hash = pair_int_hash(hash, time_formats); - hash = pair_int_hash(hash, date_time_formats); - hash = pair_int_hash(hash, available_formats); - hash = pair_int_hash(hash, default_range_format); - hash = pair_int_hash(hash, range_formats); - hash = pair_int_hash(hash, range12_formats); - hash = pair_int_hash(hash, symbols); - return hash; - } - - bool operator==(Calendar const& other) const - { - return (date_formats == other.date_formats) - && (time_formats == other.time_formats) - && (date_time_formats == other.date_time_formats) - && (available_formats == other.available_formats) - && (default_range_format == other.default_range_format) - && (range_formats == other.range_formats) - && (range12_formats == other.range12_formats) - && (symbols == other.symbols); - } - - size_t date_formats { 0 }; - size_t time_formats { 0 }; - size_t date_time_formats { 0 }; - size_t available_formats { 0 }; - - size_t default_range_format { 0 }; - size_t range_formats { 0 }; - size_t range12_formats { 0 }; - - size_t symbols { 0 }; -}; - -template<> -struct AK::Formatter : Formatter { - ErrorOr format(FormatBuilder& builder, Calendar const& calendar) - { - return Formatter::format(builder, - "{{ {}, {}, {}, {}, {}, {}, {}, {} }}"sv, - calendar.date_formats, - calendar.time_formats, - calendar.date_time_formats, - calendar.available_formats, - calendar.default_range_format, - calendar.range_formats, - calendar.range12_formats, - calendar.symbols); - } -}; - -template<> -struct AK::Traits : public DefaultTraits { - static unsigned hash(Calendar const& c) { return c.hash(); } -}; - struct TimeZoneNames { unsigned hash() const { @@ -436,46 +129,7 @@ struct AK::Traits : public DefaultTraits { static unsigned hash(TimeZoneFormat const& t) { return t.hash(); } }; -struct DayPeriod { - unsigned hash() const - { - auto hash = int_hash(static_cast(day_period)); - hash = pair_int_hash(hash, begin); - hash = pair_int_hash(hash, end); - return hash; - } - - bool operator==(DayPeriod const& other) const - { - return (day_period == other.day_period) - && (begin == other.begin) - && (end == other.end); - } - - Locale::DayPeriod day_period {}; - u8 begin { 0 }; - u8 end { 0 }; -}; - -template<> -struct AK::Formatter : Formatter { - ErrorOr format(FormatBuilder& builder, DayPeriod const& day_period) - { - return Formatter::format(builder, - "{{ {}, {}, {} }}"sv, - static_cast(day_period.day_period), - day_period.begin, - day_period.end); - } -}; - -template<> -struct AK::Traits : public DefaultTraits { - static unsigned hash(DayPeriod const& d) { return d.hash(); } -}; - using TimeZoneNamesList = Vector; -using DayPeriodList = Vector; using HourCycleList = Vector; template<> @@ -487,30 +141,15 @@ struct AK::Formatter : Formatter { }; struct LocaleData { - HashMap calendars; - size_t time_zones { 0 }; size_t time_zone_formats { 0 }; - - size_t day_periods { 0 }; }; struct CLDR { UniqueStringStorage unique_strings; - UniqueStorage unique_patterns; - UniqueStorage unique_pattern_lists; - UniqueStorage unique_range_patterns; - UniqueStorage unique_range_pattern_lists; - UniqueStorage unique_formats; - UniqueStorage unique_symbol_lists; - UniqueStorage unique_calendar_symbols; - UniqueStorage unique_calendar_symbols_lists; - UniqueStorage unique_calendars; UniqueStorage unique_time_zones; UniqueStorage unique_time_zone_lists; UniqueStorage unique_time_zone_formats; - UniqueStorage unique_day_periods; - UniqueStorage unique_day_period_lists; UniqueStorage unique_hour_cycle_lists; HashMap locales; @@ -532,37 +171,8 @@ struct CLDR { HashMap> meta_zones; Vector time_zones { "UTC"sv }; - - Vector calendars; }; -static Optional day_period_from_string(StringView day_period) -{ - if (day_period == "am"sv) - return Locale::DayPeriod::AM; - if (day_period == "pm"sv) - return Locale::DayPeriod::PM; - if (day_period == "noon"sv) - return Locale::DayPeriod::Noon; - if (day_period == "morning1"sv) - return Locale::DayPeriod::Morning1; - if (day_period == "morning2"sv) - return Locale::DayPeriod::Morning2; - if (day_period == "afternoon1"sv) - return Locale::DayPeriod::Afternoon1; - if (day_period == "afternoon2"sv) - return Locale::DayPeriod::Afternoon2; - if (day_period == "evening1"sv) - return Locale::DayPeriod::Evening1; - if (day_period == "evening2"sv) - return Locale::DayPeriod::Evening2; - if (day_period == "night1"sv) - return Locale::DayPeriod::Night1; - if (day_period == "night2"sv) - return Locale::DayPeriod::Night2; - return {}; -} - static ErrorOr parse_hour_cycles(ByteString core_path, CLDR& cldr) { // https://unicode.org/reports/tr35/tr35-dates.html#Time_Data @@ -702,787 +312,6 @@ static ErrorOr parse_meta_zones(ByteString core_path, CLDR& cldr) return {}; } -static constexpr auto is_char(char ch) -{ - return [ch](auto c) { return c == ch; }; -} - -// For patterns that are 12-hour aware, we need to generate two patterns: one with the day period -// (e.g. {ampm}) in the pattern, and one without the day period. We need to take care to remove -// extra spaces around the day period. Some example expected removals: -// -// "{hour}:{minute} {ampm}" becomes "{hour}:{minute}" (remove the space before {ampm}) -// "{ampm} {hour}" becomes "{hour}" (remove the space after {ampm}) -// "{hour}:{minute} {ampm} {timeZoneName}" becomes "{hour}:{minute} {timeZoneName}" (remove one of the spaces around {ampm}) -static ErrorOr remove_period_from_pattern(String pattern) -{ - auto is_surrounding_space = [&](auto code_point_iterator) { - if (code_point_iterator.done()) - return false; - - constexpr auto spaces = Array { static_cast(0x0020), 0x00a0, 0x2009, 0x202f }; - return spaces.span().contains_slow(*code_point_iterator); - }; - - auto is_opening = [&](auto code_point_iterator) { - if (code_point_iterator.done()) - return false; - return *code_point_iterator == '{'; - }; - - auto is_closing = [&](auto code_point_iterator) { - if (code_point_iterator.done()) - return false; - return *code_point_iterator == '}'; - }; - - for (auto remove : AK::Array { "({ampm})"sv, "{ampm}"sv, "({dayPeriod})"sv, "{dayPeriod}"sv }) { - auto index = pattern.find_byte_offset(remove); - if (!index.has_value()) - continue; - - Utf8View utf8_pattern { pattern }; - Utf8CodePointIterator before_removal; - Utf8CodePointIterator after_removal; - - for (auto it = utf8_pattern.begin(); utf8_pattern.byte_offset_of(it) < *index; ++it) - before_removal = it; - if (auto it = utf8_pattern.iterator_at_byte_offset(*index + remove.length()); it != utf8_pattern.end()) - after_removal = it; - - auto pattern_view = pattern.bytes_as_string_view(); - - if (is_surrounding_space(before_removal) && !is_opening(after_removal)) { - pattern = TRY(String::formatted("{}{}", - pattern_view.substring_view(0, *index - before_removal.underlying_code_point_length_in_bytes()), - pattern_view.substring_view(*index + remove.length()))); - } else if (is_surrounding_space(after_removal) && !is_closing(before_removal)) { - pattern = TRY(String::formatted("{}{}", - pattern_view.substring_view(0, *index), - pattern_view.substring_view(*index + remove.length() + after_removal.underlying_code_point_length_in_bytes()))); - } else { - pattern = TRY(String::formatted("{}{}", - pattern_view.substring_view(0, *index), - pattern_view.substring_view(*index + remove.length()))); - } - } - - return pattern; -} - -static ErrorOr> parse_date_time_pattern_raw(ByteString pattern, ByteString skeleton, CLDR& cldr) -{ - // https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table - using Locale::CalendarPatternStyle; - - CalendarPattern format {}; - - if (!skeleton.is_empty()) - format.skeleton_index = cldr.unique_strings.ensure(move(skeleton)); - - GenericLexer lexer { pattern }; - StringBuilder builder; - bool hour12 { false }; - - while (!lexer.is_eof()) { - // Literal strings enclosed by quotes are to be appended to the pattern as-is without further - // processing (this just avoids conflicts with the patterns below). - if (lexer.next_is(is_quote)) { - builder.append(lexer.consume_quoted_string()); - continue; - } - - auto starting_char = lexer.peek(); - auto segment = lexer.consume_while([&](char ch) { return ch == starting_char; }); - - // Era - if (all_of(segment, is_char('G'))) { - builder.append("{era}"sv); - - if (segment.length() <= 3) - format.era = CalendarPatternStyle::Short; - else if (segment.length() == 4) - format.era = CalendarPatternStyle::Long; - else - format.era = CalendarPatternStyle::Narrow; - } - - // Year - else if (all_of(segment, is_any_of("yYuUr"sv))) { - builder.append("{year}"sv); - - if (segment.length() == 2) - format.year = CalendarPatternStyle::TwoDigit; - else - format.year = CalendarPatternStyle::Numeric; - } - - // Quarter - else if (all_of(segment, is_any_of("qQ"sv))) { - // Intl.DateTimeFormat does not support quarter formatting, so drop these patterns. - return OptionalNone {}; - } - - // Month - else if (all_of(segment, is_any_of("ML"sv))) { - builder.append("{month}"sv); - - if (segment.length() == 1) - format.month = CalendarPatternStyle::Numeric; - else if (segment.length() == 2) - format.month = CalendarPatternStyle::TwoDigit; - else if (segment.length() == 3) - format.month = CalendarPatternStyle::Short; - else if (segment.length() == 4) - format.month = CalendarPatternStyle::Long; - else if (segment.length() == 5) - format.month = CalendarPatternStyle::Narrow; - } else if (all_of(segment, is_char('l'))) { - // Using 'l' for month formatting is deprecated by TR-35, ensure it is not used. - return OptionalNone {}; - } - - // Week - else if (all_of(segment, is_any_of("wW"sv))) { - // Intl.DateTimeFormat does not support week formatting, so drop these patterns. - return OptionalNone {}; - } - - // Day - else if (all_of(segment, is_char('d'))) { - builder.append("{day}"sv); - - if (segment.length() == 1) - format.day = CalendarPatternStyle::Numeric; - else - format.day = CalendarPatternStyle::TwoDigit; - } else if (all_of(segment, is_any_of("DFg"sv))) { - builder.append("{day}"sv); - format.day = CalendarPatternStyle::Numeric; - } - - // Weekday - else if (all_of(segment, is_char('E'))) { - builder.append("{weekday}"sv); - - if (segment.length() == 4) - format.weekday = CalendarPatternStyle::Long; - else if (segment.length() == 5) - format.weekday = CalendarPatternStyle::Narrow; - else - format.weekday = CalendarPatternStyle::Short; - } else if (all_of(segment, is_any_of("ec"sv))) { - builder.append("{weekday}"sv); - - // TR-35 defines "e", "c", and "cc" as as numeric, and "ee" as 2-digit, but those - // pattern styles are not supported by Intl.DateTimeFormat. - if (segment.length() <= 2) - return OptionalNone {}; - - if (segment.length() == 4) - format.weekday = CalendarPatternStyle::Long; - else if (segment.length() == 5) - format.weekday = CalendarPatternStyle::Narrow; - else - format.weekday = CalendarPatternStyle::Short; - } - - // Period - else if (all_of(segment, is_any_of("ab"sv))) { - builder.append("{ampm}"sv); - hour12 = true; - } else if (all_of(segment, is_char('B'))) { - builder.append("{dayPeriod}"sv); - hour12 = true; - - if (segment.length() == 4) - format.day_period = CalendarPatternStyle::Long; - else if (segment.length() == 5) - format.day_period = CalendarPatternStyle::Narrow; - else - format.day_period = CalendarPatternStyle::Short; - } - - // Hour - else if (all_of(segment, is_any_of("hHKk"sv))) { - builder.append("{hour}"sv); - - if ((segment[0] == 'h') || (segment[0] == 'K')) - hour12 = true; - - if (segment.length() == 1) - format.hour = CalendarPatternStyle::Numeric; - else - format.hour = CalendarPatternStyle::TwoDigit; - } else if (all_of(segment, is_any_of("jJC"sv))) { - // TR-35 indicates these should not be used. - return OptionalNone {}; - } - - // Minute - else if (all_of(segment, is_char('m'))) { - builder.append("{minute}"sv); - - if (segment.length() == 1) - format.minute = CalendarPatternStyle::Numeric; - else - format.minute = CalendarPatternStyle::TwoDigit; - } - - // Second - else if (all_of(segment, is_char('s'))) { - builder.append("{second}"sv); - - if (segment.length() == 1) - format.second = CalendarPatternStyle::Numeric; - else - format.second = CalendarPatternStyle::TwoDigit; - } else if (all_of(segment, is_char('S'))) { - builder.append("{fractionalSecondDigits}"sv); - - VERIFY(segment.length() <= 3); - format.fractional_second_digits = static_cast(segment.length()); - } else if (all_of(segment, is_char('A'))) { - // Intl.DateTimeFormat does not support millisecond formatting, so drop these patterns. - return OptionalNone {}; - } - - // Zone - else if (all_of(segment, is_any_of("zV"sv))) { - builder.append("{timeZoneName}"sv); - - if (segment.length() < 4) - format.time_zone_name = CalendarPatternStyle::Short; - else - format.time_zone_name = CalendarPatternStyle::Long; - } else if (all_of(segment, is_any_of("ZOXx"sv))) { - builder.append("{timeZoneName}"sv); - - if (segment.length() < 4) - format.time_zone_name = CalendarPatternStyle::ShortOffset; - else - format.time_zone_name = CalendarPatternStyle::LongOffset; - } else if (all_of(segment, is_char('v'))) { - builder.append("{timeZoneName}"sv); - - if (segment.length() < 4) - format.time_zone_name = CalendarPatternStyle::ShortGeneric; - else - format.time_zone_name = CalendarPatternStyle::LongGeneric; - } - - // Non-patterns - else { - builder.append(segment); - } - } - - auto parsed_pattern = TRY(builder.to_string()); - - if (hour12) { - format.pattern = TRY(remove_period_from_pattern(parsed_pattern)); - format.pattern12 = move(parsed_pattern); - } else { - format.pattern = move(parsed_pattern); - } - - return format; -} - -static ErrorOr> parse_date_time_pattern(ByteString pattern, ByteString skeleton, CLDR& cldr) -{ - auto format = TRY(parse_date_time_pattern_raw(move(pattern), move(skeleton), cldr)); - if (!format.has_value()) - return OptionalNone {}; - - format->pattern_index = cldr.unique_strings.ensure(format->pattern.to_byte_string()); - - if (format->pattern12.has_value()) - format->pattern12_index = cldr.unique_strings.ensure(format->pattern12->to_byte_string()); - - return Optional { cldr.unique_patterns.ensure(format.release_value()) }; -} - -template -static constexpr bool char_is_one_of(char ch, Chars&&... chars) -{ - return ((ch == chars) || ...); -} - -static ErrorOr parse_interval_patterns(Calendar& calendar, JsonObject const& interval_formats_object, CLDR& cldr) -{ - // https://unicode.org/reports/tr35/tr35-dates.html#intervalFormats - CalendarRangePatternList range_formats {}; - CalendarRangePatternList range12_formats {}; - - auto name_of_field = [&](char field) { - if (char_is_one_of(field, 'G')) - return Locale::CalendarRangePattern::Field::Era; - if (char_is_one_of(field, 'y', 'Y', 'u', 'U', 'r')) - return Locale::CalendarRangePattern::Field::Year; - if (char_is_one_of(field, 'M', 'L')) - return Locale::CalendarRangePattern::Field::Month; - if (char_is_one_of(field, 'd', 'D', 'F', 'g')) - return Locale::CalendarRangePattern::Field::Day; - if (char_is_one_of(field, 'a', 'b')) - return Locale::CalendarRangePattern::Field::AmPm; - if (char_is_one_of(field, 'B')) - return Locale::CalendarRangePattern::Field::DayPeriod; - if (char_is_one_of(field, 'h', 'H', 'K', 'k')) - return Locale::CalendarRangePattern::Field::Hour; - if (char_is_one_of(field, 'm')) - return Locale::CalendarRangePattern::Field::Minute; - if (char_is_one_of(field, 's')) - return Locale::CalendarRangePattern::Field::Second; - if (char_is_one_of(field, 'S')) - return Locale::CalendarRangePattern::Field::FractionalSecondDigits; - - VERIFY_NOT_REACHED(); - }; - - auto split_default_range_pattern = [&](auto skeleton, auto const& pattern) { - auto start_range_end = pattern.find('}').value() + 1; - auto end_range_begin = pattern.find_last('{').value(); - - auto start_range = pattern.substring_view(0, start_range_end); - auto separator = pattern.substring_view(start_range_end, end_range_begin - start_range_end); - auto end_range = pattern.substring_view(end_range_begin); - - CalendarRangePattern format {}; - format.skeleton_index = cldr.unique_strings.ensure(skeleton); - format.start_range = cldr.unique_strings.ensure(start_range); - format.separator = cldr.unique_strings.ensure(separator); - format.end_range = cldr.unique_strings.ensure(end_range); - - return format; - }; - - auto split_range_pattern = [&](auto skeleton, auto field, auto const& pattern, auto const& parsed_fields) { - HashMap partitions; - StringView last_partition; - - auto begin_index = pattern.find_byte_offset('{'); - size_t end_index = 0; - - while (begin_index.has_value()) { - end_index = pattern.find_byte_offset('}', *begin_index).value(); - - auto partition = pattern.bytes_as_string_view().substring_view(*begin_index, end_index - *begin_index); - if (partitions.contains(partition)) - break; - - partitions.set(partition, *begin_index); - last_partition = partition; - - begin_index = pattern.find_byte_offset('{', end_index + 1); - } - - VERIFY(begin_index.has_value() && !last_partition.is_empty()); - auto start_range_end = partitions.get(last_partition).value() + last_partition.length() + 1; - - auto pattern_view = pattern.bytes_as_string_view(); - auto start_range = pattern_view.substring_view(0, start_range_end); - auto separator = pattern_view.substring_view(start_range_end, *begin_index - start_range_end); - auto end_range = pattern_view.substring_view(*begin_index); - - CalendarRangePattern format {}; - format.skeleton_index = cldr.unique_strings.ensure(skeleton); - format.field = field; - format.start_range = cldr.unique_strings.ensure(start_range); - format.separator = cldr.unique_strings.ensure(separator); - format.end_range = cldr.unique_strings.ensure(end_range); - - format.for_each_calendar_field_zipped_with(parsed_fields, [](auto& format_field, auto const& parsed_field, auto) { - format_field = parsed_field; - }); - - return format; - }; - - TRY(interval_formats_object.try_for_each_member([&](auto const& skeleton, auto const& value) -> ErrorOr { - if (skeleton == "intervalFormatFallback"sv) { - auto range_format = split_default_range_pattern(skeleton, value.as_string()); - calendar.default_range_format = cldr.unique_range_patterns.ensure(move(range_format)); - return {}; - } - - TRY(value.as_object().try_for_each_member([&](auto const& field, auto const& pattern) -> ErrorOr { - if (field.ends_with("alt-variant"sv)) - return {}; - - VERIFY(field.length() == 1); - auto name = name_of_field(field[0]); - - auto format = TRY(parse_date_time_pattern_raw(pattern.as_string(), skeleton, cldr)).release_value(); - - auto range_format = split_range_pattern(skeleton, name, format.pattern, format); - range_formats.append(cldr.unique_range_patterns.ensure(move(range_format))); - - if (format.pattern12.has_value()) { - auto range12_pattern = split_range_pattern(skeleton, name, *format.pattern12, format); - range12_formats.append(cldr.unique_range_patterns.ensure(move(range12_pattern))); - } else { - range12_formats.append(range_formats.last()); - } - - return {}; - })); - - return {}; - })); - - calendar.range_formats = cldr.unique_range_pattern_lists.ensure(move(range_formats)); - calendar.range12_formats = cldr.unique_range_pattern_lists.ensure(move(range12_formats)); - - return {}; -} - -static ErrorOr generate_default_patterns(CalendarPatternList& formats, CLDR& cldr) -{ - // For compatibility with ICU, we generate a list of default patterns for every locale: - // https://github.com/unicode-org/icu/blob/release-71-1/icu4c/source/i18n/dtptngen.cpp#L1343-L1354= - static constexpr auto default_patterns = Array { "G"sv, "y"sv, "M"sv, "E"sv, "D"sv, "F"sv, "d"sv, "a"sv, "B"sv, "H"sv, "mm"sv, "ss"sv, "SS"sv, "v"sv }; - - for (auto pattern : default_patterns) { - auto index = TRY(parse_date_time_pattern(pattern, pattern, cldr)); - VERIFY(index.has_value()); - - if (!formats.contains_slow(*index)) - formats.append(*index); - } - - return {}; -} - -static void generate_missing_patterns(Calendar& calendar, CalendarPatternList& formats, Vector date_formats, Vector time_formats, CLDR& cldr) -{ - // https://unicode.org/reports/tr35/tr35-dates.html#Missing_Skeleton_Fields - auto replace_pattern = [&](auto format, auto time_format, auto date_format) { - auto pattern = cldr.unique_strings.get(format); - auto time_pattern = cldr.unique_strings.get(time_format); - auto date_pattern = cldr.unique_strings.get(date_format); - - auto new_pattern = pattern.replace("{0}"sv, time_pattern, ReplaceMode::FirstOnly).replace("{1}"sv, date_pattern, ReplaceMode::FirstOnly); - return cldr.unique_strings.ensure(move(new_pattern)); - }; - - auto inject_fractional_second_digits = [&](auto format) { - auto pattern = cldr.unique_strings.get(format); - - auto new_pattern = pattern.replace("{second}"sv, "{second}{decimal}{fractionalSecondDigits}"sv, ReplaceMode::FirstOnly); - return cldr.unique_strings.ensure(move(new_pattern)); - }; - - auto append_if_unique = [&](auto format) { - auto format_index = cldr.unique_patterns.ensure(move(format)); - - if (!formats.contains_slow(format_index)) - formats.append(format_index); - }; - - Vector time_formats_with_fractional_second_digits; - - for (auto const& format : date_formats) - append_if_unique(format); - for (auto const& format : time_formats) { - append_if_unique(format); - - if (format.second.has_value() && !format.fractional_second_digits.has_value()) { - auto new_format = format; - new_format.fractional_second_digits = 2; - - new_format.pattern_index = inject_fractional_second_digits(new_format.pattern_index); - if (new_format.pattern12_index != 0) - new_format.pattern12_index = inject_fractional_second_digits(new_format.pattern12_index); - - time_formats_with_fractional_second_digits.append(new_format); - append_if_unique(move(new_format)); - } - } - - time_formats.extend(move(time_formats_with_fractional_second_digits)); - - for (auto const& date_format : date_formats) { - auto const& date_time_formats = cldr.unique_formats.get(calendar.date_time_formats); - size_t date_time_format_index = 0; - - if (date_format.month == Locale::CalendarPatternStyle::Long) { - if (date_format.weekday.has_value()) - date_time_format_index = date_time_formats.full_format; - else - date_time_format_index = date_time_formats.long_format; - } else if (date_format.month == Locale::CalendarPatternStyle::Short) { - date_time_format_index = date_time_formats.medium_format; - } else { - date_time_format_index = date_time_formats.short_format; - } - - for (auto const& time_format : time_formats) { - auto format = cldr.unique_patterns.get(date_time_format_index); - - if (time_format.pattern12_index != 0) - format.pattern12_index = replace_pattern(format.pattern_index, time_format.pattern12_index, date_format.pattern_index); - format.pattern_index = replace_pattern(format.pattern_index, time_format.pattern_index, date_format.pattern_index); - - format.for_each_calendar_field_zipped_with(date_format, [](auto& field, auto const& date_field, auto) { - if (date_field.has_value()) - field = date_field; - }); - format.for_each_calendar_field_zipped_with(time_format, [](auto& field, auto const& time_field, auto) { - if (time_field.has_value()) - field = time_field; - }); - - append_if_unique(move(format)); - } - } -} - -static void parse_calendar_symbols(Calendar& calendar, JsonObject const& calendar_object, CLDR& cldr) -{ - auto create_symbol_lists = [](size_t size) { - SymbolList narrow_symbol_list; - SymbolList short_symbol_list; - SymbolList long_symbol_list; - - narrow_symbol_list.resize(size); - short_symbol_list.resize(size); - long_symbol_list.resize(size); - - return Array { { - move(narrow_symbol_list), - move(short_symbol_list), - move(long_symbol_list), - } }; - }; - - CalendarSymbolsList symbols_list {}; - - auto store_symbol_lists = [&](auto symbol, auto symbol_lists) { - auto symbol_index = to_underlying(symbol); - if (symbol_index >= symbols_list.size()) - symbols_list.resize(symbol_index + 1); - - CalendarSymbols symbols {}; - symbols.narrow_symbols = cldr.unique_symbol_lists.ensure(move(symbol_lists[0])); - symbols.short_symbols = cldr.unique_symbol_lists.ensure(move(symbol_lists[1])); - symbols.long_symbols = cldr.unique_symbol_lists.ensure(move(symbol_lists[2])); - - auto calendar_symbols_index = cldr.unique_calendar_symbols.ensure(move(symbols)); - symbols_list[symbol_index] = calendar_symbols_index; - }; - - auto parse_era_symbols = [&](auto const& symbols_object) { - auto const& narrow_symbols = symbols_object.get_object("eraNarrow"sv).value(); - auto const& short_symbols = symbols_object.get_object("eraAbbr"sv).value(); - auto const& long_symbols = symbols_object.get_object("eraNames"sv).value(); - auto symbol_lists = create_symbol_lists(2); - - auto append_symbol = [&](auto& symbols, auto const& key, auto symbol) { - if (auto key_index = key.template to_number(); key_index.has_value()) - symbols[*key_index] = cldr.unique_strings.ensure(move(symbol)); - }; - - narrow_symbols.for_each_member([&](auto const& key, JsonValue const& value) { - append_symbol(symbol_lists[0], key, value.as_string()); - }); - short_symbols.for_each_member([&](auto const& key, JsonValue const& value) { - append_symbol(symbol_lists[1], key, value.as_string()); - }); - long_symbols.for_each_member([&](auto const& key, JsonValue const& value) { - append_symbol(symbol_lists[2], key, value.as_string()); - }); - - store_symbol_lists(Locale::CalendarSymbol::Era, move(symbol_lists)); - }; - - auto parse_month_symbols = [&](auto const& symbols_object) { - auto const& narrow_symbols = symbols_object.get_object("narrow"sv).value(); - auto const& short_symbols = symbols_object.get_object("abbreviated"sv).value(); - auto const& long_symbols = symbols_object.get_object("wide"sv).value(); - auto symbol_lists = create_symbol_lists(12); - - auto append_symbol = [&](auto& symbols, auto const& key, auto symbol) { - auto key_index = key.template to_number().value() - 1; - symbols[key_index] = cldr.unique_strings.ensure(move(symbol)); - }; - - narrow_symbols.for_each_member([&](auto const& key, JsonValue const& value) { - append_symbol(symbol_lists[0], key, value.as_string()); - }); - short_symbols.for_each_member([&](auto const& key, JsonValue const& value) { - append_symbol(symbol_lists[1], key, value.as_string()); - }); - long_symbols.for_each_member([&](auto const& key, JsonValue const& value) { - append_symbol(symbol_lists[2], key, value.as_string()); - }); - - store_symbol_lists(Locale::CalendarSymbol::Month, move(symbol_lists)); - }; - - auto parse_weekday_symbols = [&](auto const& symbols_object) { - auto const& narrow_symbols = symbols_object.get_object("narrow"sv).value(); - auto const& short_symbols = symbols_object.get_object("abbreviated"sv).value(); - auto const& long_symbols = symbols_object.get_object("wide"sv).value(); - auto symbol_lists = create_symbol_lists(7); - - auto append_symbol = [&](auto& symbols, auto const& key, auto symbol) { - if (key == "sun"sv) - symbols[to_underlying(Locale::Weekday::Sunday)] = cldr.unique_strings.ensure(move(symbol)); - else if (key == "mon"sv) - symbols[to_underlying(Locale::Weekday::Monday)] = cldr.unique_strings.ensure(move(symbol)); - else if (key == "tue"sv) - symbols[to_underlying(Locale::Weekday::Tuesday)] = cldr.unique_strings.ensure(move(symbol)); - else if (key == "wed"sv) - symbols[to_underlying(Locale::Weekday::Wednesday)] = cldr.unique_strings.ensure(move(symbol)); - else if (key == "thu"sv) - symbols[to_underlying(Locale::Weekday::Thursday)] = cldr.unique_strings.ensure(move(symbol)); - else if (key == "fri"sv) - symbols[to_underlying(Locale::Weekday::Friday)] = cldr.unique_strings.ensure(move(symbol)); - else if (key == "sat"sv) - symbols[to_underlying(Locale::Weekday::Saturday)] = cldr.unique_strings.ensure(move(symbol)); - }; - - narrow_symbols.for_each_member([&](auto const& key, JsonValue const& value) { - append_symbol(symbol_lists[0], key, value.as_string()); - }); - short_symbols.for_each_member([&](auto const& key, JsonValue const& value) { - append_symbol(symbol_lists[1], key, value.as_string()); - }); - long_symbols.for_each_member([&](auto const& key, JsonValue const& value) { - append_symbol(symbol_lists[2], key, value.as_string()); - }); - - store_symbol_lists(Locale::CalendarSymbol::Weekday, move(symbol_lists)); - }; - - auto parse_day_period_symbols = [&](auto const& symbols_object) { - auto const& narrow_symbols = symbols_object.get_object("narrow"sv).value(); - auto const& short_symbols = symbols_object.get_object("abbreviated"sv).value(); - auto const& long_symbols = symbols_object.get_object("wide"sv).value(); - auto symbol_lists = create_symbol_lists(11); - - auto append_symbol = [&](auto& symbols, auto const& key, auto symbol) { - if (auto day_period = day_period_from_string(key); day_period.has_value()) - symbols[to_underlying(*day_period)] = cldr.unique_strings.ensure(move(symbol)); - }; - - narrow_symbols.for_each_member([&](auto const& key, JsonValue const& value) { - append_symbol(symbol_lists[0], key, value.as_string()); - }); - short_symbols.for_each_member([&](auto const& key, JsonValue const& value) { - append_symbol(symbol_lists[1], key, value.as_string()); - }); - long_symbols.for_each_member([&](auto const& key, JsonValue const& value) { - append_symbol(symbol_lists[2], key, value.as_string()); - }); - - store_symbol_lists(Locale::CalendarSymbol::DayPeriod, move(symbol_lists)); - }; - - parse_era_symbols(calendar_object.get_object("eras"sv).value()); - parse_month_symbols(calendar_object.get_object("months"sv)->get_object("format"sv).value()); - parse_weekday_symbols(calendar_object.get_object("days"sv)->get_object("format"sv).value()); - parse_day_period_symbols(calendar_object.get_object("dayPeriods"sv)->get_object("format"sv).value()); - - calendar.symbols = cldr.unique_calendar_symbols_lists.ensure(move(symbols_list)); -} - -static ErrorOr parse_calendars(ByteString locale_calendars_path, CLDR& cldr, LocaleData& locale) -{ - LexicalPath calendars_path(move(locale_calendars_path)); - if (!calendars_path.basename().starts_with("ca-"sv)) - return {}; - - auto calendars = TRY(read_json_file(calendars_path.string())); - auto const& main_object = calendars.as_object().get_object("main"sv).value(); - auto const& locale_object = main_object.get_object(calendars_path.parent().basename()).value(); - auto const& dates_object = locale_object.get_object("dates"sv).value(); - auto const& calendars_object = dates_object.get_object("calendars"sv).value(); - - auto parse_patterns = [&](auto const& patterns_object, auto const& skeletons_object, Vector* patterns) -> ErrorOr { - auto parse_pattern = [&](auto name) -> ErrorOr { - auto format = patterns_object.get_byte_string(name); - auto skeleton = skeletons_object.get_byte_string(name); - - auto format_index = TRY(parse_date_time_pattern(format.value(), skeleton.value_or(ByteString::empty()), cldr)).value(); - - if (patterns) - patterns->append(cldr.unique_patterns.get(format_index)); - - return format_index; - }; - - CalendarFormat formats {}; - formats.full_format = TRY(parse_pattern("full"sv)); - formats.long_format = TRY(parse_pattern("long"sv)); - formats.medium_format = TRY(parse_pattern("medium"sv)); - formats.short_format = TRY(parse_pattern("short"sv)); - - return cldr.unique_formats.ensure(move(formats)); - }; - - TRY(calendars_object.try_for_each_member([&](auto const& calendar_name, JsonValue const& value) -> ErrorOr { - // The generic calendar is not a supported Unicode calendar key, so skip it: - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/calendar#unicode_calendar_keys - if (calendar_name == "generic"sv) - return {}; - - Calendar calendar {}; - CalendarPatternList available_formats {}; - - if (!cldr.calendars.contains_slow(calendar_name)) - cldr.calendars.append(calendar_name); - - Vector date_formats; - Vector time_formats; - - auto const& date_formats_object = value.as_object().get_object("dateFormats"sv).value(); - auto const& date_skeletons_object = value.as_object().get_object("dateSkeletons"sv).value(); - calendar.date_formats = TRY(parse_patterns(date_formats_object, date_skeletons_object, &date_formats)); - - auto const& time_formats_object = value.as_object().get_object("timeFormats"sv).value(); - auto const& time_skeletons_object = value.as_object().get_object("timeSkeletons"sv).value(); - calendar.time_formats = TRY(parse_patterns(time_formats_object, time_skeletons_object, &time_formats)); - - auto const& standard_date_time_formats_object = value.as_object().get_object("dateTimeFormats-atTime"sv)->get_object("standard"sv).value(); - calendar.date_time_formats = TRY(parse_patterns(standard_date_time_formats_object, JsonObject {}, nullptr)); - - auto const& date_time_formats_object = value.as_object().get_object("dateTimeFormats"sv).value(); - auto const& available_formats_object = date_time_formats_object.get_object("availableFormats"sv).value(); - TRY(available_formats_object.try_for_each_member([&](auto const& skeleton, JsonValue const& pattern) -> ErrorOr { - auto pattern_index = TRY(parse_date_time_pattern(pattern.as_string(), skeleton, cldr)); - if (!pattern_index.has_value()) - return {}; - - auto const& format = cldr.unique_patterns.get(*pattern_index); - if (format.contains_only_date_fields()) - date_formats.append(format); - else if (format.contains_only_time_fields()) - time_formats.append(format); - - if (!available_formats.contains_slow(*pattern_index)) - available_formats.append(*pattern_index); - - return {}; - })); - - auto const& interval_formats_object = date_time_formats_object.get_object("intervalFormats"sv).value(); - TRY(parse_interval_patterns(calendar, interval_formats_object, cldr)); - - TRY(generate_default_patterns(available_formats, cldr)); - generate_missing_patterns(calendar, available_formats, move(date_formats), move(time_formats), cldr); - parse_calendar_symbols(calendar, value.as_object(), cldr); - - calendar.available_formats = cldr.unique_pattern_lists.ensure(move(available_formats)); - locale.calendars.set(calendar_name, cldr.unique_calendars.ensure(move(calendar))); - - return {}; - })); - - return {}; -} - static ErrorOr parse_time_zone_names(ByteString locale_time_zone_names_path, CLDR& cldr, LocaleData& locale) { LexicalPath time_zone_names_path(move(locale_time_zone_names_path)); @@ -1592,62 +421,6 @@ static ErrorOr parse_time_zone_names(ByteString locale_time_zone_names_pat return {}; } -static ErrorOr parse_day_periods(ByteString core_path, CLDR& cldr) -{ - // https://unicode.org/reports/tr35/tr35-dates.html#Day_Period_Rule_Sets - LexicalPath day_periods_path(move(core_path)); - day_periods_path = day_periods_path.append("supplemental"sv); - day_periods_path = day_periods_path.append("dayPeriods.json"sv); - - auto locale_day_periods = TRY(read_json_file(day_periods_path.string())); - auto const& supplemental_object = locale_day_periods.as_object().get_object("supplemental"sv).value(); - auto const& day_periods_object = supplemental_object.get_object("dayPeriodRuleSet"sv).value(); - - auto parse_hour = [](auto const& time) { - auto hour_end_index = time.find(':').value(); - - // The times are in the form "hours:minutes", but we only need the hour segment. - // TR-35 explicitly states that minutes other than :00 must not be used. - VERIFY(time.substring_view(hour_end_index) == ":00"sv); - - auto hour = time.substring_view(0, hour_end_index); - return hour.template to_number().value(); - }; - - auto parse_day_period = [&](auto const& symbol, auto const& ranges) -> Optional { - if (!ranges.has("_from"sv)) - return {}; - - auto day_period = day_period_from_string(symbol); - if (!day_period.has_value()) - return {}; - - auto begin = parse_hour(ranges.get_byte_string("_from"sv).value()); - auto end = parse_hour(ranges.get_byte_string("_before"sv).value()); - - return DayPeriod { *day_period, begin, end }; - }; - - day_periods_object.for_each_member([&](auto const& language, JsonValue const& value) { - auto locale = cldr.locales.find(language); - if (locale == cldr.locales.end()) - return; - - DayPeriodList day_periods; - - value.as_object().for_each_member([&](auto const& symbol, JsonValue const& ranges) { - if (auto day_period = parse_day_period(symbol, ranges.as_object()); day_period.has_value()) { - auto day_period_index = cldr.unique_day_periods.ensure(day_period.release_value()); - day_periods.append(day_period_index); - } - }); - - locale->value.day_periods = cldr.unique_day_period_lists.ensure(move(day_periods)); - }); - - return {}; -} - static ErrorOr parse_all_locales(ByteString core_path, ByteString dates_path, CLDR& cldr) { TRY(parse_hour_cycles(core_path, cldr)); @@ -1673,17 +446,10 @@ static ErrorOr parse_all_locales(ByteString core_path, ByteString dates_pa auto language = TRY(remove_variants_from_path(dates_path)); auto& locale = cldr.locales.ensure(language); - TRY(Core::Directory::for_each_entry(dates_path, Core::DirIterator::SkipParentAndBaseDir, [&](auto& dates_entry, auto& dates_directory) -> ErrorOr { - auto calendars_path = LexicalPath::join(dates_directory.path().string(), dates_entry.name).string(); - TRY(parse_calendars(move(calendars_path), cldr, locale)); - return IterationDecision::Continue; - })); - TRY(parse_time_zone_names(move(dates_path), cldr, locale)); return IterationDecision::Continue; })); - TRY(parse_day_periods(move(core_path), cldr)); return {}; } @@ -1712,7 +478,6 @@ static ErrorOr generate_unicode_locale_header(Core::InputBufferedFile& fil namespace Locale { )~~~"); - generate_enum(generator, format_identifier, "Calendar"sv, {}, cldr.calendars); generate_enum(generator, format_identifier, "HourCycleRegion"sv, {}, cldr.hour_cycle_regions); generate_enum(generator, format_identifier, "MinimumDaysRegion"sv, {}, cldr.minimum_days_regions); generate_enum(generator, format_identifier, "FirstDayRegion"sv, {}, cldr.first_day_regions); @@ -1732,19 +497,8 @@ 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("calendar_pattern_index_type"sv, cldr.unique_patterns.type_that_fits()); - generator.set("calendar_pattern_list_index_type"sv, cldr.unique_pattern_lists.type_that_fits()); - generator.set("calendar_range_pattern_index_type"sv, cldr.unique_range_patterns.type_that_fits()); - generator.set("calendar_range_pattern_list_index_type"sv, cldr.unique_range_pattern_lists.type_that_fits()); - generator.set("calendar_format_index_type"sv, cldr.unique_formats.type_that_fits()); - generator.set("symbol_list_index_type"sv, cldr.unique_symbol_lists.type_that_fits()); - generator.set("calendar_symbols_index_type"sv, cldr.unique_calendar_symbols.type_that_fits()); - generator.set("calendar_symbols_list_index_type"sv, cldr.unique_calendar_symbols_lists.type_that_fits()); - generator.set("calendar_index_type"sv, cldr.unique_calendars.type_that_fits()); generator.set("time_zone_index_type"sv, cldr.unique_time_zones.type_that_fits()); generator.set("time_zone_list_index_type"sv, cldr.unique_time_zone_lists.type_that_fits()); - generator.set("day_period_index_type"sv, cldr.unique_day_periods.type_that_fits()); - generator.set("day_period_list_index_type"sv, cldr.unique_day_period_lists.type_that_fits()); generator.append(R"~~~( #include @@ -1765,140 +519,6 @@ namespace Locale { cldr.unique_strings.generate(generator); generator.append(R"~~~( -template -static void convert_calendar_fields(SourceType const& source, TargetType& target) -{ - if (source.era != -1) - target.era = static_cast(source.era); - if (source.year != -1) - target.year = static_cast(source.year); - if (source.month != -1) - target.month = static_cast(source.month); - if (source.weekday != -1) - target.weekday = static_cast(source.weekday); - if (source.day != -1) - target.day = static_cast(source.day); - if (source.day_period != -1) - target.day_period = static_cast(source.day_period); - if (source.hour != -1) - target.hour = static_cast(source.hour); - if (source.minute != -1) - target.minute = static_cast(source.minute); - if (source.second != -1) - target.second = static_cast(source.second); - if (source.fractional_second_digits != -1) - target.fractional_second_digits = static_cast(source.fractional_second_digits); - if (source.time_zone_name != -1) - target.time_zone_name = static_cast(source.time_zone_name); -} - -struct CalendarPatternImpl { - CalendarPattern to_unicode_calendar_pattern() const { - CalendarPattern calendar_pattern {}; - - calendar_pattern.skeleton = String::from_utf8_without_validation(decode_string(skeleton).bytes()); - calendar_pattern.pattern = String::from_utf8_without_validation(decode_string(pattern).bytes()); - if (pattern12 != 0) - calendar_pattern.pattern12 = String::from_utf8_without_validation(decode_string(pattern12).bytes()); - - convert_calendar_fields(*this, calendar_pattern); - return calendar_pattern; - } - - @string_index_type@ skeleton { 0 }; - @string_index_type@ pattern { 0 }; - @string_index_type@ pattern12 { 0 }; - - i8 era { -1 }; - i8 year { -1 }; - i8 month { -1 }; - i8 weekday { -1 }; - i8 day { -1 }; - i8 day_period { -1 }; - i8 hour { -1 }; - i8 minute { -1 }; - i8 second { -1 }; - i8 fractional_second_digits { -1 }; - i8 time_zone_name { -1 }; -}; - -struct CalendarRangePatternImpl { - CalendarRangePattern to_unicode_calendar_range_pattern() const { - CalendarRangePattern calendar_range_pattern {}; - - if (field != -1) - calendar_range_pattern.field = static_cast(field); - calendar_range_pattern.start_range = String::from_utf8_without_validation(decode_string(start_range).bytes()); - calendar_range_pattern.separator = decode_string(separator); - calendar_range_pattern.end_range = String::from_utf8_without_validation(decode_string(end_range).bytes()); - - convert_calendar_fields(*this, calendar_range_pattern); - return calendar_range_pattern; - } - - @string_index_type@ skeleton { 0 }; - i8 field { -1 }; - @string_index_type@ start_range { 0 }; - @string_index_type@ separator { 0 }; - @string_index_type@ end_range { 0 }; - - i8 era { -1 }; - i8 year { -1 }; - i8 month { -1 }; - i8 weekday { -1 }; - i8 day { -1 }; - i8 day_period { -1 }; - i8 hour { -1 }; - i8 minute { -1 }; - i8 second { -1 }; - i8 fractional_second_digits { -1 }; - i8 time_zone_name { -1 }; -}; -)~~~"); - - cldr.unique_patterns.generate(generator, "CalendarPatternImpl"sv, "s_calendar_patterns"sv, 10); - cldr.unique_pattern_lists.generate(generator, cldr.unique_patterns.type_that_fits(), "s_calendar_pattern_lists"sv); - cldr.unique_range_patterns.generate(generator, "CalendarRangePatternImpl"sv, "s_calendar_range_patterns"sv, 10); - cldr.unique_range_pattern_lists.generate(generator, cldr.unique_range_patterns.type_that_fits(), "s_calendar_range_pattern_lists"sv); - - generator.append(R"~~~( -struct CalendarFormatImpl { - CalendarFormat to_unicode_calendar_format() const { - CalendarFormat calendar_format {}; - - calendar_format.full_format = s_calendar_patterns[full_format].to_unicode_calendar_pattern(); - calendar_format.long_format = s_calendar_patterns[long_format].to_unicode_calendar_pattern(); - calendar_format.medium_format = s_calendar_patterns[medium_format].to_unicode_calendar_pattern(); - calendar_format.short_format = s_calendar_patterns[short_format].to_unicode_calendar_pattern(); - - return calendar_format; - } - - @calendar_pattern_index_type@ full_format { 0 }; - @calendar_pattern_index_type@ long_format { 0 }; - @calendar_pattern_index_type@ medium_format { 0 }; - @calendar_pattern_index_type@ short_format { 0 }; -}; - -struct CalendarSymbols { - @symbol_list_index_type@ narrow_symbols { 0 }; - @symbol_list_index_type@ short_symbols { 0 }; - @symbol_list_index_type@ long_symbols { 0 }; -}; - -struct CalendarData { - @calendar_format_index_type@ date_formats { 0 }; - @calendar_format_index_type@ time_formats { 0 }; - @calendar_format_index_type@ date_time_formats { 0 }; - @calendar_pattern_list_index_type@ available_formats { 0 }; - - @calendar_range_pattern_index_type@ default_range_format { 0 }; - @calendar_range_pattern_list_index_type@ range_formats { 0 }; - @calendar_range_pattern_list_index_type@ range12_formats { 0 }; - - @calendar_symbols_list_index_type@ symbols { 0 }; -}; - struct TimeZoneNames { @string_index_type@ short_standard_name { 0 }; @string_index_type@ long_standard_name { 0 }; @@ -1933,45 +553,13 @@ struct TimeZoneFormatImpl { @string_index_type@ gmt_format { 0 }; @string_index_type@ gmt_zero_format { 0 }; }; - -struct DayPeriodData { - u8 day_period { 0 }; - u8 begin { 0 }; - u8 end { 0 }; -}; )~~~"); - cldr.unique_formats.generate(generator, "CalendarFormatImpl"sv, "s_calendar_formats"sv, 10); - cldr.unique_symbol_lists.generate(generator, cldr.unique_strings.type_that_fits(), "s_symbol_lists"sv); - cldr.unique_calendar_symbols.generate(generator, "CalendarSymbols"sv, "s_calendar_symbols"sv, 10); - cldr.unique_calendar_symbols_lists.generate(generator, cldr.unique_calendar_symbols.type_that_fits(), "s_calendar_symbol_lists"sv); - cldr.unique_calendars.generate(generator, "CalendarData"sv, "s_calendars"sv, 10); cldr.unique_time_zones.generate(generator, "TimeZoneNames"sv, "s_time_zones"sv, 30); cldr.unique_time_zone_lists.generate(generator, cldr.unique_time_zones.type_that_fits(), "s_time_zone_lists"sv); cldr.unique_time_zone_formats.generate(generator, "TimeZoneFormatImpl"sv, "s_time_zone_formats"sv, 30); - cldr.unique_day_periods.generate(generator, "DayPeriodData"sv, "s_day_periods"sv, 30); - cldr.unique_day_period_lists.generate(generator, cldr.unique_day_periods.type_that_fits(), "s_day_period_lists"sv); cldr.unique_hour_cycle_lists.generate(generator, cldr.unique_hour_cycle_lists.type_that_fits(), "s_hour_cycle_lists"sv); - auto append_calendars = [&](ByteString name, auto const& calendars) { - generator.set("name", name); - generator.set("size", ByteString::number(calendars.size())); - - generator.append(R"~~~( -static constexpr Array<@calendar_index_type@, @size@> @name@ { {)~~~"); - - bool first = true; - for (auto const& calendar_key : cldr.calendars) { - auto calendar = calendars.find(calendar_key)->value; - - generator.append(first ? " "sv : ", "sv); - generator.append(ByteString::number(calendar)); - first = false; - } - - generator.append(" } };"); - }; - auto append_mapping = [&](auto const& keys, auto const& map, auto type, auto name, auto mapping_getter) { generator.set("type", type); generator.set("name", name); @@ -1996,10 +584,8 @@ static constexpr Array<@type@, @size@> @name@ { {)~~~"); auto locales = cldr.locales.keys(); quick_sort(locales); - generate_mapping(generator, cldr.locales, cldr.unique_calendars.type_that_fits(), "s_locale_calendars"sv, "s_calendars_{}"sv, format_identifier, [&](auto const& name, auto const& value) { append_calendars(name, value.calendars); }); append_mapping(locales, cldr.locales, cldr.unique_time_zones.type_that_fits(), "s_locale_time_zones"sv, [](auto const& locale) { return locale.time_zones; }); append_mapping(locales, cldr.locales, cldr.unique_time_zone_formats.type_that_fits(), "s_locale_time_zone_formats"sv, [](auto const& locale) { return locale.time_zone_formats; }); - append_mapping(locales, cldr.locales, cldr.unique_day_periods.type_that_fits(), "s_locale_day_periods"sv, [](auto const& locale) { return locale.day_periods; }); append_mapping(cldr.hour_cycle_regions, cldr.hour_cycles, cldr.unique_hour_cycle_lists.type_that_fits(), "s_hour_cycles"sv, [](auto const& hour_cycles) { return hour_cycles; }); append_mapping(cldr.minimum_days_regions, cldr.minimum_days, "u8"sv, "s_minimum_days"sv, [](auto minimum_days) { return minimum_days; }); append_mapping(cldr.first_day_regions, cldr.first_day, "u8"sv, "s_first_day"sv, [](auto first_day) { return to_underlying(first_day); }); @@ -2028,23 +614,6 @@ static constexpr Array<@type@, @size@> @name@ { {)~~~"); TRY(append_from_string("WeekendEndRegion"sv, "weekend_end_region"sv, cldr.weekend_end_regions)); generator.append(R"~~~( -static Optional keyword_to_calendar(KeywordCalendar keyword) -{ - switch (keyword) {)~~~"); - - for (auto const& calendar : cldr.calendars) { - generator.set("name"sv, format_identifier({}, calendar)); - generator.append(R"~~~( - case KeywordCalendar::@name@: - return Calendar::@name@;)~~~"); - } - - generator.append(R"~~~( - default: - return {}; - } -} - Vector get_regional_hour_cycles(StringView region) { auto region_value = hour_cycle_region_from_string(region); @@ -2091,244 +660,6 @@ Optional<@return_type@> get_regional_@lookup_type@(StringView region) append_regional_lookup("Weekday"sv, "weekend_end"sv); generator.append(R"~~~( -static CalendarData const* find_calendar_data(StringView locale, StringView calendar) -{ - 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& calendar_indices = s_locale_calendars.at(locale_index); - - auto lookup_calendar = [&](auto calendar_name) -> CalendarData const* { - auto calendar_keyword = keyword_ca_from_string(calendar_name); - if (!calendar_keyword.has_value()) - return nullptr; - - auto calendar_value = keyword_to_calendar(*calendar_keyword); - if (!calendar_value.has_value()) - return nullptr; - - size_t calendar_index = to_underlying(*calendar_value); - calendar_index = calendar_indices[calendar_index]; - - return &s_calendars[calendar_index]; - }; - - if (auto const* calendar_data = lookup_calendar(calendar)) - return calendar_data; - - auto default_calendar = get_preferred_keyword_value_for_locale(locale, "ca"sv); - if (!default_calendar.has_value()) - return nullptr; - - return lookup_calendar(*default_calendar); -} - -Optional get_calendar_date_format(StringView locale, StringView calendar) -{ - if (auto const* data = find_calendar_data(locale, calendar); data != nullptr) { - auto const& formats = s_calendar_formats.at(data->date_formats); - return formats.to_unicode_calendar_format(); - } - return {}; -} - -Optional get_calendar_time_format(StringView locale, StringView calendar) -{ - if (auto const* data = find_calendar_data(locale, calendar); data != nullptr) { - auto const& formats = s_calendar_formats.at(data->time_formats); - return formats.to_unicode_calendar_format(); - } - return {}; -} - -Optional get_calendar_date_time_format(StringView locale, StringView calendar) -{ - if (auto const* data = find_calendar_data(locale, calendar); data != nullptr) { - auto const& formats = s_calendar_formats.at(data->date_time_formats); - return formats.to_unicode_calendar_format(); - } - return {}; -} - -Vector get_calendar_available_formats(StringView locale, StringView calendar) -{ - Vector result {}; - - if (auto const* data = find_calendar_data(locale, calendar); data != nullptr) { - auto const& available_formats = s_calendar_pattern_lists.at(data->available_formats); - result.ensure_capacity(available_formats.size()); - - for (auto const& format : available_formats) - result.unchecked_append(s_calendar_patterns[format].to_unicode_calendar_pattern()); - } - - return result; -} - -Optional get_calendar_default_range_format(StringView locale, StringView calendar) -{ - if (auto const* data = find_calendar_data(locale, calendar); data != nullptr) { - auto const& pattern = s_calendar_range_patterns[data->default_range_format]; - return pattern.to_unicode_calendar_range_pattern(); - } - - return {}; -} - -Vector get_calendar_range_formats(StringView locale, StringView calendar, StringView skeleton) -{ - Vector result {}; - - if (auto const* data = find_calendar_data(locale, calendar); data != nullptr) { - auto const& range_formats = s_calendar_range_pattern_lists.at(data->range_formats); - - for (auto format : range_formats) { - auto const& pattern = s_calendar_range_patterns[format]; - - if (skeleton == decode_string(pattern.skeleton)) - result.append(pattern.to_unicode_calendar_range_pattern()); - } - } - - return result; -} - -Vector get_calendar_range12_formats(StringView locale, StringView calendar, StringView skeleton) -{ - Vector result {}; - - if (auto const* data = find_calendar_data(locale, calendar); data != nullptr) { - auto const& range12_formats = s_calendar_range_pattern_lists.at(data->range12_formats); - - for (auto format : range12_formats) { - auto const& pattern = s_calendar_range_patterns[format]; - - if (skeleton == decode_string(pattern.skeleton)) - result.append(pattern.to_unicode_calendar_range_pattern()); - } - } - - return result; -} - -static ReadonlySpan<@string_index_type@> find_calendar_symbols(StringView locale, StringView calendar, CalendarSymbol symbol, CalendarPatternStyle style) -{ - if (auto const* data = find_calendar_data(locale, calendar); data != nullptr) { - auto const& symbols_list = s_calendar_symbol_lists[data->symbols]; - auto symbol_index = to_underlying(symbol); - - auto calendar_symbols_index = symbols_list.at(symbol_index); - auto const& symbols = s_calendar_symbols.at(calendar_symbols_index); - - @symbol_list_index_type@ symbol_list_index = 0; - - switch (style) { - case CalendarPatternStyle::Narrow: - symbol_list_index = symbols.narrow_symbols; - break; - case CalendarPatternStyle::Short: - symbol_list_index = symbols.short_symbols; - break; - case CalendarPatternStyle::Long: - symbol_list_index = symbols.long_symbols; - break; - default: - VERIFY_NOT_REACHED(); - } - - return s_symbol_lists.at(symbol_list_index); - } - - return {}; -} - -Optional get_calendar_era_symbol(StringView locale, StringView calendar, CalendarPatternStyle style, Era value) -{ - auto symbols = find_calendar_symbols(locale, calendar, CalendarSymbol::Era, style); - - if (auto value_index = to_underlying(value); value_index < symbols.size()) { - if (auto symbol_index = symbols.at(value_index); symbol_index != 0) - return decode_string(symbol_index); - } - - return {}; -} - -Optional get_calendar_month_symbol(StringView locale, StringView calendar, CalendarPatternStyle style, Month value) -{ - auto symbols = find_calendar_symbols(locale, calendar, CalendarSymbol::Month, style); - - if (auto value_index = to_underlying(value); value_index < symbols.size()) { - if (auto symbol_index = symbols.at(value_index); symbol_index != 0) - return decode_string(symbol_index); - } - - return {}; -} - -Optional get_calendar_weekday_symbol(StringView locale, StringView calendar, CalendarPatternStyle style, Weekday value) -{ - auto symbols = find_calendar_symbols(locale, calendar, CalendarSymbol::Weekday, style); - - if (auto value_index = to_underlying(value); value_index < symbols.size()) { - if (auto symbol_index = symbols.at(value_index); symbol_index != 0) - return decode_string(symbol_index); - } - - return {}; -} - -Optional get_calendar_day_period_symbol(StringView locale, StringView calendar, CalendarPatternStyle style, DayPeriod value) -{ - auto symbols = find_calendar_symbols(locale, calendar, CalendarSymbol::DayPeriod, style); - - if (auto value_index = to_underlying(value); value_index < symbols.size()) { - if (auto symbol_index = symbols.at(value_index); symbol_index != 0) - return decode_string(symbol_index); - } - - return {}; -} - -Optional get_calendar_day_period_symbol_for_hour(StringView locale, StringView calendar, CalendarPatternStyle style, u8 hour) -{ - auto locale_value = locale_from_string(locale); - if (!locale_value.has_value()) - return {}; - - auto locale_index = to_underlying(*locale_value) - 1; // Subtract 1 because 0 == Locale::None. - - auto day_periods_index = s_locale_day_periods[locale_index]; - auto day_periods = s_day_period_lists[day_periods_index]; - - for (auto day_period_index : day_periods) { - auto day_period = s_day_periods[day_period_index]; - bool hour_falls_within_day_period = false; - - if (day_period.begin > day_period.end) { - if (hour >= day_period.begin) - hour_falls_within_day_period = true; - else if (hour <= day_period.end) - hour_falls_within_day_period = true; - } else if ((day_period.begin <= hour) && (hour < day_period.end)) { - hour_falls_within_day_period = true; - } - - if (hour_falls_within_day_period) { - auto period = static_cast(day_period.day_period); - return get_calendar_day_period_symbol(locale, calendar, style, period); - } - } - - // Fallback to fixed periods if the locale does not have flexible day periods. - // TR-35 states that the meaning of AM and PM does not change with locale. - if (hour < 12) - return get_calendar_day_period_symbol(locale, calendar, style, DayPeriod::AM); - return get_calendar_day_period_symbol(locale, calendar, style, DayPeriod::PM); -} - Optional get_time_zone_format(StringView locale) { auto locale_value = locale_from_string(locale); diff --git a/Tests/LibLocale/CMakeLists.txt b/Tests/LibLocale/CMakeLists.txt index e31bf262b4e..07159780a67 100644 --- a/Tests/LibLocale/CMakeLists.txt +++ b/Tests/LibLocale/CMakeLists.txt @@ -1,11 +1,8 @@ set(TEST_SOURCES - TestDateTimeFormat.cpp TestDisplayNames.cpp TestLocale.cpp ) foreach(source IN LISTS TEST_SOURCES) serenity_test("${source}" LibLocale LIBS LibLocale) - - get_filename_component(target "${source}" NAME_WLE) endforeach() diff --git a/Tests/LibLocale/TestDateTimeFormat.cpp b/Tests/LibLocale/TestDateTimeFormat.cpp deleted file mode 100644 index 0941fdb4925..00000000000 --- a/Tests/LibLocale/TestDateTimeFormat.cpp +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright (c) 2022, Tim Flynn - * - * SPDX-License-Identifier: BSD-2-Clause - */ - -#include - -#include -#include -#include -#include - -TEST_CASE(time_zone_name) -{ - struct TestData { - StringView locale; - Locale::CalendarPatternStyle style; - StringView time_zone; - StringView expected_result; - }; - - constexpr auto test_data = Array { - TestData { "en"sv, Locale::CalendarPatternStyle::Long, "UTC"sv, "Coordinated Universal Time"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::Short, "UTC"sv, "UTC"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::LongGeneric, "UTC"sv, "GMT"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::ShortGeneric, "UTC"sv, "GMT"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "UTC"sv, "التوقيت العالمي المنسق"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "UTC"sv, "UTC"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::LongGeneric, "UTC"sv, "غرينتش"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::ShortGeneric, "UTC"sv, "غرينتش"sv }, - - TestData { "en"sv, Locale::CalendarPatternStyle::Long, "America/Los_Angeles"sv, "Pacific Standard Time"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::Short, "America/Los_Angeles"sv, "PST"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::LongGeneric, "America/Los_Angeles"sv, "Pacific Time"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::ShortGeneric, "America/Los_Angeles"sv, "PT"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "America/Los_Angeles"sv, "توقيت المحيط الهادي الرسمي"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "America/Los_Angeles"sv, "غرينتش-٨"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::LongGeneric, "America/Los_Angeles"sv, "توقيت المحيط الهادي"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::ShortGeneric, "America/Los_Angeles"sv, "غرينتش-٨"sv }, - - TestData { "en"sv, Locale::CalendarPatternStyle::Long, "America/Vancouver"sv, "Pacific Standard Time"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::Short, "America/Vancouver"sv, "PST"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::LongGeneric, "America/Vancouver"sv, "Pacific Time"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::ShortGeneric, "America/Vancouver"sv, "PT"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "America/Vancouver"sv, "توقيت المحيط الهادي الرسمي"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "America/Vancouver"sv, "غرينتش-٨"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::LongGeneric, "America/Vancouver"sv, "توقيت المحيط الهادي"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::ShortGeneric, "America/Vancouver"sv, "غرينتش-٨"sv }, - - TestData { "en"sv, Locale::CalendarPatternStyle::Long, "Europe/London"sv, "Greenwich Mean Time"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::Short, "Europe/London"sv, "GMT"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::LongGeneric, "Europe/London"sv, "GMT"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::ShortGeneric, "Europe/London"sv, "GMT"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "Europe/London"sv, "توقيت غرينتش"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "Europe/London"sv, "غرينتش"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::LongGeneric, "Europe/London"sv, "غرينتش"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::ShortGeneric, "Europe/London"sv, "غرينتش"sv }, - - TestData { "en"sv, Locale::CalendarPatternStyle::Long, "Africa/Accra"sv, "Greenwich Mean Time"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::Short, "Africa/Accra"sv, "GMT"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::LongGeneric, "Africa/Accra"sv, "GMT"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::ShortGeneric, "Africa/Accra"sv, "GMT"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "Africa/Accra"sv, "توقيت غرينتش"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "Africa/Accra"sv, "غرينتش"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::LongGeneric, "Africa/Accra"sv, "غرينتش"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::ShortGeneric, "Africa/Accra"sv, "غرينتش"sv }, - }; - - constexpr auto jan_1_2022 = AK::UnixDateTime::from_seconds_since_epoch(1640995200); // Saturday, January 1, 2022 12:00:00 AM - - for (auto const& test : test_data) { - auto time_zone = Locale::format_time_zone(test.locale, test.time_zone, test.style, jan_1_2022); - EXPECT_EQ(time_zone, test.expected_result); - } -} - -TEST_CASE(time_zone_name_dst) -{ - struct TestData { - StringView locale; - Locale::CalendarPatternStyle style; - StringView time_zone; - StringView expected_result; - }; - - constexpr auto test_data = Array { - TestData { "en"sv, Locale::CalendarPatternStyle::Long, "UTC"sv, "Coordinated Universal Time"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::Short, "UTC"sv, "UTC"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "UTC"sv, "التوقيت العالمي المنسق"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "UTC"sv, "UTC"sv }, - - TestData { "en"sv, Locale::CalendarPatternStyle::Long, "America/Los_Angeles"sv, "Pacific Daylight Time"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::Short, "America/Los_Angeles"sv, "PDT"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "America/Los_Angeles"sv, "توقيت المحيط الهادي الصيفي"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "America/Los_Angeles"sv, "غرينتش-٧"sv }, - - TestData { "en"sv, Locale::CalendarPatternStyle::Long, "America/Vancouver"sv, "Pacific Daylight Time"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::Short, "America/Vancouver"sv, "PDT"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "America/Vancouver"sv, "توقيت المحيط الهادي الصيفي"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "America/Vancouver"sv, "غرينتش-٧"sv }, - - // FIXME: This should be "British Summer Time", but the CLDR puts that one name in a section we aren't parsing. - TestData { "en"sv, Locale::CalendarPatternStyle::Long, "Europe/London"sv, "GMT+01:00"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::Short, "Europe/London"sv, "GMT+1"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "Europe/London"sv, "غرينتش+٠١:٠٠"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "Europe/London"sv, "غرينتش+١"sv }, - - TestData { "en"sv, Locale::CalendarPatternStyle::Long, "Africa/Accra"sv, "Greenwich Mean Time"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::Short, "Africa/Accra"sv, "GMT"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::Long, "Africa/Accra"sv, "توقيت غرينتش"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::Short, "Africa/Accra"sv, "غرينتش"sv }, - }; - - constexpr auto sep_19_2022 = AK::UnixDateTime::from_seconds_since_epoch(1663553728); // Monday, September 19, 2022 2:15:28 AM - - for (auto const& test : test_data) { - auto time_zone = Locale::format_time_zone(test.locale, test.time_zone, test.style, sep_19_2022); - EXPECT_EQ(time_zone, test.expected_result); - } -} - -TEST_CASE(format_time_zone_offset) -{ - constexpr auto jan_1_1833 = AK::UnixDateTime::from_seconds_since_epoch(-4323283200); // Tuesday, January 1, 1833 12:00:00 AM - constexpr auto jan_1_2022 = AK::UnixDateTime::from_seconds_since_epoch(1640995200); // Saturday, January 1, 2022 12:00:00 AM - - struct TestData { - StringView locale; - Locale::CalendarPatternStyle style; - AK::UnixDateTime time; - StringView time_zone; - StringView expected_result; - }; - - constexpr auto test_data = Array { - TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, {}, "UTC"sv, "GMT"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, {}, "UTC"sv, "GMT"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, {}, "UTC"sv, "غرينتش"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, {}, "UTC"sv, "غرينتش"sv }, - - TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_1833, "America/Los_Angeles"sv, "GMT-7:52:58"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_2022, "America/Los_Angeles"sv, "GMT-8"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_1833, "America/Los_Angeles"sv, "GMT-07:52:58"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_2022, "America/Los_Angeles"sv, "GMT-08:00"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_1833, "America/Los_Angeles"sv, "غرينتش-٧:٥٢:٥٨"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_2022, "America/Los_Angeles"sv, "غرينتش-٨"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_1833, "America/Los_Angeles"sv, "غرينتش-٠٧:٥٢:٥٨"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_2022, "America/Los_Angeles"sv, "غرينتش-٠٨:٠٠"sv }, - - TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_1833, "Europe/London"sv, "GMT-0:01:15"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_2022, "Europe/London"sv, "GMT"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_1833, "Europe/London"sv, "GMT-00:01:15"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_2022, "Europe/London"sv, "GMT"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_1833, "Europe/London"sv, "غرينتش-٠:٠١:١٥"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_2022, "Europe/London"sv, "غرينتش"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_1833, "Europe/London"sv, "غرينتش-٠٠:٠١:١٥"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_2022, "Europe/London"sv, "غرينتش"sv }, - - TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_1833, "Asia/Kathmandu"sv, "GMT+5:41:16"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_2022, "Asia/Kathmandu"sv, "GMT+5:45"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_1833, "Asia/Kathmandu"sv, "GMT+05:41:16"sv }, - TestData { "en"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_2022, "Asia/Kathmandu"sv, "GMT+05:45"sv }, - - TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_1833, "Asia/Kathmandu"sv, "غرينتش+٥:٤١:١٦"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::ShortOffset, jan_1_2022, "Asia/Kathmandu"sv, "غرينتش+٥:٤٥"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_1833, "Asia/Kathmandu"sv, "غرينتش+٠٥:٤١:١٦"sv }, - TestData { "ar"sv, Locale::CalendarPatternStyle::LongOffset, jan_1_2022, "Asia/Kathmandu"sv, "غرينتش+٠٥:٤٥"sv }, - }; - - for (auto const& test : test_data) { - auto time_zone = Locale::format_time_zone(test.locale, test.time_zone, test.style, test.time); - EXPECT_EQ(time_zone, test.expected_result); - } -} diff --git a/Userland/Libraries/LibJS/Print.cpp b/Userland/Libraries/LibJS/Print.cpp index bc01bea40be..635ccc0b96e 100644 --- a/Userland/Libraries/LibJS/Print.cpp +++ b/Userland/Libraries/LibJS/Print.cpp @@ -736,15 +736,13 @@ ErrorOr print_intl_date_time_format(JS::PrintContext& print_context, JS::I TRY(print_type(print_context, "Intl.DateTimeFormat"sv)); TRY(js_out(print_context, "\n locale: ")); TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), date_time_format.locale()), seen_objects)); - TRY(js_out(print_context, "\n pattern: ")); - TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), date_time_format.pattern()), seen_objects)); TRY(js_out(print_context, "\n calendar: ")); TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), date_time_format.calendar()), seen_objects)); TRY(js_out(print_context, "\n numberingSystem: ")); TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), date_time_format.numbering_system()), seen_objects)); - if (date_time_format.has_hour_cycle()) { + if (date_time_format.hour_cycle.has_value()) { TRY(js_out(print_context, "\n hourCycle: ")); - TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), date_time_format.hour_cycle_string()), seen_objects)); + TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), ::Locale::hour_cycle_to_string(*date_time_format.hour_cycle)), seen_objects)); } TRY(js_out(print_context, "\n timeZone: ")); TRY(print_value(print_context, JS::PrimitiveString::create(date_time_format.vm(), date_time_format.time_zone()), seen_objects)); diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp index a85ba1227c1..6ed9c186539 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.cpp @@ -1,33 +1,19 @@ /* - * Copyright (c) 2021-2023, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ -#include -#include -#include -#include -#include -#include #include #include #include -#include -#include #include -#include -#include -#include -#include #include namespace JS::Intl { JS_DEFINE_ALLOCATOR(DateTimeFormat); -static Crypto::SignedBigInteger const s_one_million_bigint { 1'000'000 }; - // 11 DateTimeFormat Objects, https://tc39.es/ecma402/#datetimeformat-objects DateTimeFormat::DateTimeFormat(Object& prototype) : Object(ConstructWithPrototypeTag::Tag, prototype) @@ -37,425 +23,12 @@ DateTimeFormat::DateTimeFormat(Object& prototype) void DateTimeFormat::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); - if (m_bound_format) - visitor.visit(m_bound_format); -} - -DateTimeFormat::Style DateTimeFormat::style_from_string(StringView style) -{ - if (style == "full"sv) - return Style::Full; - if (style == "long"sv) - return Style::Long; - if (style == "medium"sv) - return Style::Medium; - if (style == "short"sv) - return Style::Short; - VERIFY_NOT_REACHED(); -} - -StringView DateTimeFormat::style_to_string(Style style) -{ - switch (style) { - case Style::Full: - return "full"sv; - case Style::Long: - return "long"sv; - case Style::Medium: - return "medium"sv; - case Style::Short: - return "short"sv; - default: - VERIFY_NOT_REACHED(); - } -} - -// 11.5.1 DateTimeStyleFormat ( dateStyle, timeStyle, styles ), https://tc39.es/ecma402/#sec-date-time-style-format -Optional<::Locale::CalendarPattern> date_time_style_format(StringView data_locale, DateTimeFormat& date_time_format) -{ - ::Locale::CalendarPattern time_format {}; - ::Locale::CalendarPattern date_format {}; - - auto get_pattern = [&](auto type, auto style) -> Optional<::Locale::CalendarPattern> { - auto formats = ::Locale::get_calendar_format(data_locale, date_time_format.calendar(), type); - - if (formats.has_value()) { - switch (style) { - case DateTimeFormat::Style::Full: - return formats->full_format; - case DateTimeFormat::Style::Long: - return formats->long_format; - case DateTimeFormat::Style::Medium: - return formats->medium_format; - case DateTimeFormat::Style::Short: - return formats->short_format; - } - } - - return {}; - }; - - // 1. If timeStyle is not undefined, then - if (date_time_format.has_time_style()) { - // a. Assert: timeStyle is one of "full", "long", "medium", or "short". - // b. Let timeFormat be styles.[[TimeFormat]].[[]]. - auto pattern = get_pattern(::Locale::CalendarFormatType::Time, date_time_format.time_style()); - if (!pattern.has_value()) - return {}; - - time_format = pattern.release_value(); - } - - // 2. If dateStyle is not undefined, then - if (date_time_format.has_date_style()) { - // a. Assert: dateStyle is one of "full", "long", "medium", or "short". - // b. Let dateFormat be styles.[[DateFormat]].[[]]. - auto pattern = get_pattern(::Locale::CalendarFormatType::Date, date_time_format.date_style()); - if (!pattern.has_value()) - return {}; - - date_format = pattern.release_value(); - } - - // 3. If dateStyle is not undefined and timeStyle is not undefined, then - if (date_time_format.has_date_style() && date_time_format.has_time_style()) { - // a. Let format be a new Record. - ::Locale::CalendarPattern format {}; - - // b. Add to format all fields from dateFormat except [[pattern]] and [[rangePatterns]]. - format.for_each_calendar_field_zipped_with(date_format, [](auto& format_field, auto const& date_format_field, auto) { - format_field = date_format_field; - }); - - // c. Add to format all fields from timeFormat except [[pattern]], [[rangePatterns]], [[pattern12]], and [[rangePatterns12]], if present. - format.for_each_calendar_field_zipped_with(time_format, [](auto& format_field, auto const& time_format_field, auto) { - if (time_format_field.has_value()) - format_field = time_format_field; - }); - - // d. Let connector be styles.[[DateTimeFormat]].[[]]. - auto connector = get_pattern(::Locale::CalendarFormatType::DateTime, date_time_format.date_style()); - if (!connector.has_value()) - return {}; - - // e. Let pattern be the string connector with the substring "{0}" replaced with timeFormat.[[pattern]] and the substring "{1}" replaced with dateFormat.[[pattern]]. - auto pattern = MUST(connector->pattern.replace("{0}"sv, time_format.pattern, ReplaceMode::FirstOnly)); - pattern = MUST(pattern.replace("{1}"sv, date_format.pattern, ReplaceMode::FirstOnly)); - - // f. Set format.[[pattern]] to pattern. - format.pattern = move(pattern); - - // g. If timeFormat has a [[pattern12]] field, then - if (time_format.pattern12.has_value()) { - // i. Let pattern12 be the string connector with the substring "{0}" replaced with timeFormat.[[pattern12]] and the substring "{1}" replaced with dateFormat.[[pattern]]. - auto pattern12 = MUST(connector->pattern.replace("{0}"sv, *time_format.pattern12, ReplaceMode::FirstOnly)); - pattern12 = MUST(pattern12.replace("{1}"sv, date_format.pattern, ReplaceMode::FirstOnly)); - - // ii. Set format.[[pattern12]] to pattern12. - format.pattern12 = move(pattern12); - } - - // NOTE: Our implementation of steps h-j differ from the spec. LibUnicode does not attach range patterns to the - // format pattern; rather, lookups for range patterns are performed separately based on the format pattern's - // skeleton. So we form a new skeleton here and defer the range pattern lookups. - format.skeleton = ::Locale::combine_skeletons(date_format.skeleton, time_format.skeleton); - - // k. Return format. - return format; - } - - // 4. If timeStyle is not undefined, then - if (date_time_format.has_time_style()) { - // a. Return timeFormat. - return time_format; - } - - // 5. Assert: dateStyle is not undefined. - VERIFY(date_time_format.has_date_style()); - - // 6. Return dateFormat. - return date_format; -} - -// 11.5.2 BasicFormatMatcher ( options, formats ), https://tc39.es/ecma402/#sec-basicformatmatcher -Optional<::Locale::CalendarPattern> basic_format_matcher(::Locale::CalendarPattern const& options, Vector<::Locale::CalendarPattern> formats) -{ - // 1. Let removalPenalty be 120. - constexpr int removal_penalty = 120; - - // 2. Let additionPenalty be 20. - constexpr int addition_penalty = 20; - - // 3. Let longLessPenalty be 8. - constexpr int long_less_penalty = 8; - - // 4. Let longMorePenalty be 6. - constexpr int long_more_penalty = 6; - - // 5. Let shortLessPenalty be 6. - constexpr int short_less_penalty = 6; - - // 6. Let shortMorePenalty be 3. - constexpr int short_more_penalty = 3; - - // 7. Let offsetPenalty be 1. - constexpr int offset_penalty = 1; - - // 8. Let bestScore be -Infinity. - int best_score = NumericLimits::min(); - - // 9. Let bestFormat be undefined. - Optional<::Locale::CalendarPattern> best_format; - - // 10. Assert: Type(formats) is List. - // 11. For each element format of formats, do - for (auto& format : formats) { - // a. Let score be 0. - int score = 0; - - // b. For each property name property shown in Table 6, do - format.for_each_calendar_field_zipped_with(options, [&](auto const& format_prop, auto const& options_prop, auto type) { - using ValueType = typename RemoveReference::ValueType; - - // i. If options has a field [[]], let optionsProp be options.[[]]; else let optionsProp be undefined. - // ii. If format has a field [[]], let formatProp be format.[[]]; else let formatProp be undefined. - - // iii. If optionsProp is undefined and formatProp is not undefined, decrease score by additionPenalty. - if (!options_prop.has_value() && format_prop.has_value()) { - score -= addition_penalty; - } - // iv. Else if optionsProp is not undefined and formatProp is undefined, decrease score by removalPenalty. - else if (options_prop.has_value() && !format_prop.has_value()) { - score -= removal_penalty; - } - // v. Else if property is "timeZoneName", then - else if (type == ::Locale::CalendarPattern::Field::TimeZoneName) { - // This is needed to avoid a compile error. Although we only enter this branch for TimeZoneName, - // the lambda we are in will be generated with property types other than CalendarPatternStyle. - auto compare_prop = [](auto prop, auto test) { return prop == static_cast(test); }; - - // 1. If optionsProp is "short" or "shortGeneric", then - if (compare_prop(options_prop, ::Locale::CalendarPatternStyle::Short) || compare_prop(options_prop, ::Locale::CalendarPatternStyle::ShortGeneric)) { - // a. If formatProp is "shortOffset", decrease score by offsetPenalty. - if (compare_prop(format_prop, ::Locale::CalendarPatternStyle::ShortOffset)) - score -= offset_penalty; - // b. Else if formatProp is "longOffset", decrease score by (offsetPenalty + shortMorePenalty). - else if (compare_prop(format_prop, ::Locale::CalendarPatternStyle::LongOffset)) - score -= offset_penalty + short_more_penalty; - // c. Else if optionsProp is "short" and formatProp is "long", decrease score by shortMorePenalty. - else if (compare_prop(options_prop, ::Locale::CalendarPatternStyle::Short) || compare_prop(format_prop, ::Locale::CalendarPatternStyle::Long)) - score -= short_more_penalty; - // d. Else if optionsProp is "shortGeneric" and formatProp is "longGeneric", decrease score by shortMorePenalty. - else if (compare_prop(options_prop, ::Locale::CalendarPatternStyle::ShortGeneric) || compare_prop(format_prop, ::Locale::CalendarPatternStyle::LongGeneric)) - score -= short_more_penalty; - // e. Else if optionsProp ≠ formatProp, decrease score by removalPenalty. - else if (options_prop != format_prop) - score -= removal_penalty; - } - // 2. Else if optionsProp is "shortOffset" and formatProp is "longOffset", decrease score by shortMorePenalty. - else if (compare_prop(options_prop, ::Locale::CalendarPatternStyle::ShortOffset) || compare_prop(format_prop, ::Locale::CalendarPatternStyle::LongOffset)) { - score -= short_more_penalty; - } - // 3. Else if optionsProp is "long" or "longGeneric", then - else if (compare_prop(options_prop, ::Locale::CalendarPatternStyle::Long) || compare_prop(options_prop, ::Locale::CalendarPatternStyle::LongGeneric)) { - // a. If formatProp is "longOffset", decrease score by offsetPenalty. - if (compare_prop(format_prop, ::Locale::CalendarPatternStyle::LongOffset)) - score -= offset_penalty; - // b. Else if formatProp is "shortOffset", decrease score by (offsetPenalty + longLessPenalty). - else if (compare_prop(format_prop, ::Locale::CalendarPatternStyle::ShortOffset)) - score -= offset_penalty + long_less_penalty; - // c. Else if optionsProp is "long" and formatProp is "short", decrease score by longLessPenalty. - else if (compare_prop(options_prop, ::Locale::CalendarPatternStyle::Long) || compare_prop(format_prop, ::Locale::CalendarPatternStyle::Short)) - score -= long_less_penalty; - // d. Else if optionsProp is "longGeneric" and formatProp is "shortGeneric", decrease score by longLessPenalty. - else if (compare_prop(options_prop, ::Locale::CalendarPatternStyle::LongGeneric) || compare_prop(format_prop, ::Locale::CalendarPatternStyle::ShortGeneric)) - score -= long_less_penalty; - // e. Else if optionsProp ≠ formatProp, decrease score by removalPenalty. - else if (options_prop != format_prop) - score -= removal_penalty; - } - // 4. Else if optionsProp is "longOffset" and formatProp is "shortOffset", decrease score by longLessPenalty. - else if (compare_prop(options_prop, ::Locale::CalendarPatternStyle::LongOffset) || compare_prop(format_prop, ::Locale::CalendarPatternStyle::ShortOffset)) { - score -= long_less_penalty; - } - // 5. Else if optionsProp ≠ formatProp, decrease score by removalPenalty. - else if (options_prop != format_prop) { - score -= removal_penalty; - } - } - // vi. Else if optionsProp ≠ formatProp, then - else if (options_prop != format_prop) { - using ValuesType = Conditional, AK::Array, AK::Array<::Locale::CalendarPatternStyle, 5>>; - ValuesType values {}; - - // 1. If property is "fractionalSecondDigits", then - if constexpr (IsIntegral) { - // a. Let values be « 1𝔽, 2𝔽, 3𝔽 ». - values = { 1, 2, 3 }; - } - // 2. Else, - else { - // a. Let values be « "2-digit", "numeric", "narrow", "short", "long" ». - values = { - ::Locale::CalendarPatternStyle::TwoDigit, - ::Locale::CalendarPatternStyle::Numeric, - ::Locale::CalendarPatternStyle::Narrow, - ::Locale::CalendarPatternStyle::Short, - ::Locale::CalendarPatternStyle::Long, - }; - } - - // 3. Let optionsPropIndex be the index of optionsProp within values. - auto options_prop_index = static_cast(find_index(values.begin(), values.end(), *options_prop)); - - // 4. Let formatPropIndex be the index of formatProp within values. - auto format_prop_index = static_cast(find_index(values.begin(), values.end(), *format_prop)); - - // 5. Let delta be max(min(formatPropIndex - optionsPropIndex, 2), -2). - int delta = max(min(format_prop_index - options_prop_index, 2), -2); - - // 6. If delta = 2, decrease score by longMorePenalty. - if (delta == 2) - score -= long_more_penalty; - // 7. Else if delta = 1, decrease score by shortMorePenalty. - else if (delta == 1) - score -= short_more_penalty; - // 8. Else if delta = -1, decrease score by shortLessPenalty. - else if (delta == -1) - score -= short_less_penalty; - // 9. Else if delta = -2, decrease score by longLessPenalty. - else if (delta == -2) - score -= long_less_penalty; - } - }); - - // c. If score > bestScore, then - if (score > best_score) { - // i. Let bestScore be score. - best_score = score; - - // ii. Let bestFormat be format. - best_format = format; - } - } - - if (!best_format.has_value()) - return {}; - - // Non-standard, if the user provided options that differ from the best format's options, keep - // the user's options. This is expected by TR-35: - // - // It is not necessary to supply dateFormatItems with skeletons for every field length; fields - // in the skeleton and pattern are expected to be expanded in parallel to handle a request. - // https://unicode.org/reports/tr35/tr35-dates.html#Matching_Skeletons - // - // Rather than generating an prohibitively large amount of nearly-duplicate patterns, which only - // differ by field length, we expand the field lengths here. - best_format->for_each_calendar_field_zipped_with(options, [&](auto& best_format_field, auto const& option_field, auto field_type) { - switch (field_type) { - case ::Locale::CalendarPattern::Field::FractionalSecondDigits: - if ((best_format_field.has_value() || best_format->second.has_value()) && option_field.has_value()) - best_format_field = option_field; - break; - - case ::Locale::CalendarPattern::Field::Hour: - case ::Locale::CalendarPattern::Field::Minute: - case ::Locale::CalendarPattern::Field::Second: - break; - - default: - if (best_format_field.has_value() && option_field.has_value()) - best_format_field = option_field; - break; - } - }); - - // 12. Return bestFormat. - return best_format; -} - -// 11.5.3 BestFitFormatMatcher ( options, formats ), https://tc39.es/ecma402/#sec-bestfitformatmatcher -Optional<::Locale::CalendarPattern> best_fit_format_matcher(::Locale::CalendarPattern const& options, Vector<::Locale::CalendarPattern> formats) -{ - // When the BestFitFormatMatcher abstract operation is called with two arguments options and formats, it performs - // implementation dependent steps, which should return a set of component representations that a typical user of - // the selected locale would perceive as at least as good as the one returned by BasicFormatMatcher. - return basic_format_matcher(options, move(formats)); -} - -struct StyleAndValue { - StringView name {}; - ::Locale::CalendarPatternStyle style {}; - i32 value { 0 }; -}; - -static Optional find_calendar_field(StringView name, ::Locale::CalendarPattern const& options, ::Locale::CalendarPattern const* range_options, LocalTime const& local_time) -{ - auto make_style_and_value = [](auto name, auto style, auto fallback_style, auto value) { - if (style.has_value()) - return StyleAndValue { name, *style, static_cast(value) }; - return StyleAndValue { name, fallback_style, static_cast(value) }; - }; - - constexpr auto weekday = "weekday"sv; - constexpr auto era = "era"sv; - constexpr auto year = "year"sv; - constexpr auto month = "month"sv; - constexpr auto day = "day"sv; - constexpr auto hour = "hour"sv; - constexpr auto minute = "minute"sv; - constexpr auto second = "second"sv; - - Optional<::Locale::CalendarPatternStyle> empty; - - if (name == weekday) - return make_style_and_value(weekday, range_options ? range_options->weekday : empty, *options.weekday, local_time.weekday); - if (name == era) - return make_style_and_value(era, range_options ? range_options->era : empty, *options.era, local_time.era); - if (name == year) - return make_style_and_value(year, range_options ? range_options->year : empty, *options.year, local_time.year); - if (name == month) - return make_style_and_value(month, range_options ? range_options->month : empty, *options.month, local_time.month); - if (name == day) - return make_style_and_value(day, range_options ? range_options->day : empty, *options.day, local_time.day); - if (name == hour) - return make_style_and_value(hour, range_options ? range_options->hour : empty, *options.hour, local_time.hour); - if (name == minute) - return make_style_and_value(minute, range_options ? range_options->minute : empty, *options.minute, local_time.minute); - if (name == second) - return make_style_and_value(second, range_options ? range_options->second : empty, *options.second, local_time.second); - return {}; -} - -static Optional resolve_day_period(StringView locale, StringView calendar, ::Locale::CalendarPatternStyle style, ReadonlySpan pattern_parts, LocalTime local_time) -{ - // Use the "noon" day period if the locale has it, but only if the time is either exactly 12:00.00 or would be displayed as such. - if (local_time.hour == 12) { - auto it = find_if(pattern_parts.begin(), pattern_parts.end(), [&](auto const& part) { - if (part.type == "minute"sv && local_time.minute != 0) - return true; - if (part.type == "second"sv && local_time.second != 0) - return true; - if (part.type == "fractionalSecondDigits"sv && local_time.millisecond != 0) - return true; - return false; - }); - - if (it == pattern_parts.end()) { - auto noon_symbol = ::Locale::get_calendar_day_period_symbol(locale, calendar, style, ::Locale::DayPeriod::Noon); - if (noon_symbol.has_value()) - return *noon_symbol; - } - } - - return ::Locale::get_calendar_day_period_symbol_for_hour(locale, calendar, style, local_time.hour); + visitor.visit(m_bound_format); } // 11.5.5 FormatDateTimePattern ( dateTimeFormat, patternParts, x, rangeFormatOptions ), https://tc39.es/ecma402/#sec-formatdatetimepattern -ThrowCompletionOr> format_date_time_pattern(VM& vm, DateTimeFormat& date_time_format, Vector pattern_parts, double time, ::Locale::CalendarPattern const* range_format_options) +ThrowCompletionOr> format_date_time_pattern(VM& vm, DateTimeFormat& date_time_format, double time) { - auto& realm = *vm.current_realm(); - // 1. Let x be TimeClip(x). time = time_clip(time); @@ -463,313 +36,38 @@ ThrowCompletionOr> format_date_time_pattern(VM& vm, Dat if (isnan(time)) return vm.throw_completion(ErrorType::IntlInvalidTime); - // 3. Let locale be dateTimeFormat.[[Locale]]. - auto const& locale = date_time_format.locale(); - auto const& data_locale = date_time_format.data_locale(); - - auto construct_number_format = [&](auto& options) -> ThrowCompletionOr { - auto number_format = TRY(construct(vm, realm.intrinsics().intl_number_format_constructor(), PrimitiveString::create(vm, locale), options)); - return static_cast(number_format.ptr()); - }; - - // 4. Let nfOptions be OrdinaryObjectCreate(null). - auto number_format_options = Object::create(realm, nullptr); - - // 5. Perform ! CreateDataPropertyOrThrow(nfOptions, "useGrouping", false). - MUST(number_format_options->create_data_property_or_throw(vm.names.useGrouping, Value(false))); - - // 6. Let nf be ? Construct(%NumberFormat%, « locale, nfOptions »). - auto* number_format = TRY(construct_number_format(number_format_options)); - - // 7. Let nf2Options be OrdinaryObjectCreate(null). - auto number_format_options2 = Object::create(realm, nullptr); - - // 8. Perform ! CreateDataPropertyOrThrow(nf2Options, "minimumIntegerDigits", 2). - MUST(number_format_options2->create_data_property_or_throw(vm.names.minimumIntegerDigits, Value(2))); - - // 9. Perform ! CreateDataPropertyOrThrow(nf2Options, "useGrouping", false). - MUST(number_format_options2->create_data_property_or_throw(vm.names.useGrouping, Value(false))); - - // 10. Let nf2 be ? Construct(%NumberFormat%, « locale, nf2Options »). - auto* number_format2 = TRY(construct_number_format(number_format_options2)); - - // 11. Let fractionalSecondDigits be dateTimeFormat.[[FractionalSecondDigits]]. - Optional fractional_second_digits; - NumberFormat* number_format3 = nullptr; - - // 12. If fractionalSecondDigits is not undefined, then - if (date_time_format.has_fractional_second_digits()) { - fractional_second_digits = date_time_format.fractional_second_digits(); - - // a. Let nf3Options be OrdinaryObjectCreate(null). - auto number_format_options3 = Object::create(realm, nullptr); - - // b. Perform ! CreateDataPropertyOrThrow(nf3Options, "minimumIntegerDigits", fractionalSecondDigits). - MUST(number_format_options3->create_data_property_or_throw(vm.names.minimumIntegerDigits, Value(*fractional_second_digits))); - - // c. Perform ! CreateDataPropertyOrThrow(nf3Options, "useGrouping", false). - MUST(number_format_options3->create_data_property_or_throw(vm.names.useGrouping, Value(false))); - - // d. Let nf3 be ? Construct(%NumberFormat%, « locale, nf3Options »). - number_format3 = TRY(construct_number_format(number_format_options3)); - } - - // 13. Let tm be ToLocalTime(ℤ(ℝ(x) × 10^6), dateTimeFormat.[[Calendar]], dateTimeFormat.[[TimeZone]]). - auto time_bigint = Crypto::SignedBigInteger { time }.multiplied_by(s_one_million_bigint); - auto local_time = TRY(to_local_time(vm, time_bigint, date_time_format.calendar(), date_time_format.time_zone())); - - // 14. Let result be a new empty List. - Vector result; - - // 15. For each Record { [[Type]], [[Value]] } patternPart in 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 the list result. - result.append({ "literal"sv, move(pattern_part.value) }); - } - - // c. Else if p is equal to "fractionalSecondDigits", then - else if (part == "fractionalSecondDigits"sv) { - // i. Let v be tm.[[Millisecond]]. - auto value = local_time.millisecond; - - // ii. Let v be floor(v × 10^(fractionalSecondDigits - 3)). - value = floor(value * pow(10, static_cast(*fractional_second_digits) - 3)); - - // iii. Let fv be FormatNumeric(nf3, v). - 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) }); - } - - // d. Else if p is equal to "dayPeriod", then - else if (part == "dayPeriod"sv) { - String formatted_value; - - // i. Let f be the value of dateTimeFormat's internal slot whose name is the Internal Slot column of the matching row. - auto style = date_time_format.day_period(); - - // ii. Let fv be a String value representing the day period of tm in the form given by f; the String value depends upon the implementation and the effective locale of dateTimeFormat. - auto symbol = resolve_day_period(data_locale, date_time_format.calendar(), style, pattern_parts, local_time); - if (symbol.has_value()) - formatted_value = MUST(String::from_utf8(*symbol)); - - // iii. Append a new Record { [[Type]]: p, [[Value]]: fv } as the last element of the list result. - result.append({ "dayPeriod"sv, move(formatted_value) }); - } - - // e. Else if p is equal to "timeZoneName", then - else if (part == "timeZoneName"sv) { - // i. Let f be dateTimeFormat.[[TimeZoneName]]. - auto style = date_time_format.time_zone_name(); - - // ii. Let v be dateTimeFormat.[[TimeZone]]. - auto const& value = date_time_format.time_zone(); - - // iii. Let fv be a String value representing v in the form given by f; the String value depends upon the implementation and the effective locale of dateTimeFormat. - // The String value may also depend on the value of the [[InDST]] field of tm if f is "short", "long", "shortOffset", or "longOffset". - // If the implementation does not have a localized representation of f, then use the String value of v itself. - auto formatted_value = ::Locale::format_time_zone(data_locale, value, style, local_time.time_since_epoch()); - - // iv. Append a new Record { [[Type]]: p, [[Value]]: fv } as the last element of the list result. - result.append({ "timeZoneName"sv, move(formatted_value) }); - } - - // f. Else if p matches a Property column of the row in Table 6, then - else if (auto style_and_value = find_calendar_field(part, date_time_format, range_format_options, local_time); style_and_value.has_value()) { - String formatted_value; - - // i. If rangeFormatOptions is not undefined, let f be the value of rangeFormatOptions's field whose name matches p. - // ii. Else, let f be the value of dateTimeFormat's internal slot whose name is the Internal Slot column of the matching row. - // NOTE: find_calendar_field handles resolving rangeFormatOptions and dateTimeFormat fields. - auto style = style_and_value->style; - - // iii. Let v be the value of tm's field whose name is the Internal Slot column of the matching row. - auto value = style_and_value->value; - - // iv. If p is "year" and v ≤ 0, let v be 1 - v. - if ((part == "year"sv) && (value <= 0)) - value = 1 - value; - - // v. If p is "month", increase v by 1. - if (part == "month"sv) - ++value; - - if (part == "hour"sv) { - auto hour_cycle = date_time_format.hour_cycle(); - - // vi. If p is "hour" and dateTimeFormat.[[HourCycle]] is "h11" or "h12", then - if ((hour_cycle == ::Locale::HourCycle::H11) || (hour_cycle == ::Locale::HourCycle::H12)) { - // 1. Let v be v modulo 12. - value = value % 12; - - // 2. If v is 0 and dateTimeFormat.[[HourCycle]] is "h12", let v be 12. - if ((value == 0) && (hour_cycle == ::Locale::HourCycle::H12)) - value = 12; - } - - // vii. If p is "hour" and dateTimeFormat.[[HourCycle]] is "h24", then - if (hour_cycle == ::Locale::HourCycle::H24) { - // 1. If v is 0, let v be 24. - if (value == 0) - value = 24; - } - } - - switch (style) { - // viii. If f is "numeric", then - case ::Locale::CalendarPatternStyle::Numeric: - // 1. Let fv be FormatNumeric(nf, v). - 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(*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. - if (formatted_value.bytes_as_string_view().length() > 2) { - auto utf16_formatted_value = Utf16String::create(formatted_value); - if (utf16_formatted_value.length_in_code_units() > 2) - formatted_value = MUST(utf16_formatted_value.substring_view(utf16_formatted_value.length_in_code_units() - 2).to_utf8()); - } - - break; - - // x. Else if f is "narrow", "short", or "long", then let fv be a String value representing v in the form given by f; the String value depends upon the implementation and the effective locale and calendar of dateTimeFormat. - // If p is "month" and rangeFormatOptions is undefined, then the String value may also depend on whether dateTimeFormat.[[Day]] is undefined. - // If p is "month" and rangeFormatOptions is not undefined, then the String value may also depend on whether rangeFormatOptions.[[day]] is undefined. - // If p is "era" and rangeFormatOptions is undefined, then the String value may also depend on whether dateTimeFormat.[[Era]] is undefined. - // If p is "era" and rangeFormatOptions is not undefined, then the String value may also depend on whether rangeFormatOptions.[[era]] is undefined. - // If the implementation does not have a localized representation of f, then use the String value of v itself. - case ::Locale::CalendarPatternStyle::Narrow: - case ::Locale::CalendarPatternStyle::Short: - case ::Locale::CalendarPatternStyle::Long: { - Optional symbol; - - if (part == "era"sv) - symbol = ::Locale::get_calendar_era_symbol(data_locale, date_time_format.calendar(), style, static_cast<::Locale::Era>(value)); - else if (part == "month"sv) - symbol = ::Locale::get_calendar_month_symbol(data_locale, date_time_format.calendar(), style, static_cast<::Locale::Month>(value - 1)); - else if (part == "weekday"sv) - symbol = ::Locale::get_calendar_weekday_symbol(data_locale, date_time_format.calendar(), style, static_cast<::Locale::Weekday>(value)); - - if (symbol.has_value()) - formatted_value = MUST(String::from_utf8(*symbol)); - else - formatted_value = MUST(String::number(value)); - - break; - } - - default: - VERIFY_NOT_REACHED(); - } - - // xi. Append a new Record { [[Type]]: p, [[Value]]: fv } as the last element of the list result. - result.append({ style_and_value->name, move(formatted_value) }); - } - - // g. Else if p is equal to "ampm", then - else if (part == "ampm"sv) { - String formatted_value; - - // i. Let v be tm.[[Hour]]. - auto value = local_time.hour; - - // ii. If v is greater than 11, then - if (value > 11) { - // 1. Let fv be an implementation and locale dependent String value representing "post meridiem". - auto symbol = ::Locale::get_calendar_day_period_symbol(data_locale, date_time_format.calendar(), ::Locale::CalendarPatternStyle::Short, ::Locale::DayPeriod::PM); - formatted_value = MUST(String::from_utf8(symbol.value_or("PM"sv))); - } - // iii. Else, - else { - // 1. Let fv be an implementation and locale dependent String value representing "ante meridiem". - auto symbol = ::Locale::get_calendar_day_period_symbol(data_locale, date_time_format.calendar(), ::Locale::CalendarPatternStyle::Short, ::Locale::DayPeriod::AM); - formatted_value = MUST(String::from_utf8(symbol.value_or("AM"sv))); - } - - // iv. Append a new Record { [[Type]]: "dayPeriod", [[Value]]: fv } as the last element of the list result. - result.append({ "dayPeriod"sv, move(formatted_value) }); - } - - // h. Else if p is equal to "relatedYear", then - else if (part == "relatedYear"sv) { - // i. Let v be tm.[[RelatedYear]]. - // ii. Let fv be FormatNumeric(nf, v). - // iii. Append a new Record { [[Type]]: "relatedYear", [[Value]]: fv } as the last element of the list result. - - // FIXME: Implement this when relatedYear is supported. - } - - // i. Else if p is equal to "yearName", then - else if (part == "yearName"sv) { - // i. Let v be tm.[[YearName]]. - // ii. Let fv be an implementation and locale dependent String value representing v. - // iii. Append a new Record { [[Type]]: "yearName", [[Value]]: fv } as the last element of the list result. - - // FIXME: Implement this when yearName is supported. - } - - // Non-standard, TR-35 requires the decimal separator before injected {fractionalSecondDigits} partitions - // to adhere to the selected locale. This depends on other generated data, so it is deferred to here. - else if (part == "decimal"sv) { - auto decimal_symbol = ::Locale::get_number_system_symbol(data_locale, date_time_format.numbering_system(), ::Locale::NumericSymbol::Decimal).value_or("."sv); - result.append({ "literal"sv, MUST(String::from_utf8(decimal_symbol)) }); - } - - // j. Else, - else { - // i. Let unknown be an implementation-, locale-, and numbering system-dependent 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(); - } - } - - // 16. Return result. - return result; + return date_time_format.formatter().format_to_parts(time); } // 11.5.6 PartitionDateTimePattern ( dateTimeFormat, x ), https://tc39.es/ecma402/#sec-partitiondatetimepattern -ThrowCompletionOr> partition_date_time_pattern(VM& vm, DateTimeFormat& date_time_format, double time) +ThrowCompletionOr> partition_date_time_pattern(VM& vm, DateTimeFormat& date_time_format, double time) { // 1. Let patternParts be PartitionPattern(dateTimeFormat.[[Pattern]]). - auto pattern_parts = partition_pattern(date_time_format.pattern()); - // 2. Let result be ? FormatDateTimePattern(dateTimeFormat, patternParts, x, undefined). - auto result = TRY(format_date_time_pattern(vm, date_time_format, move(pattern_parts), time, nullptr)); - - // 3. Return result. - return result; + return format_date_time_pattern(vm, date_time_format, time); } // 11.5.7 FormatDateTime ( dateTimeFormat, x ), https://tc39.es/ecma402/#sec-formatdatetime ThrowCompletionOr format_date_time(VM& vm, DateTimeFormat& date_time_format, double time) { // 1. Let parts be ? PartitionDateTimePattern(dateTimeFormat, x). - auto parts = TRY(partition_date_time_pattern(vm, date_time_format, time)); + { + // NOTE: We short-circuit PartitionDateTimePattern as we do not need individual partitions. But we must still + // perform the time clip and NaN sanity checks from its call to FormatDateTimePattern. - // 2. Let result be the empty String. - StringBuilder result; + // 1. Let x be TimeClip(x). + time = time_clip(time); - // 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); + // 2. If x is NaN, throw a RangeError exception. + if (isnan(time)) + return vm.throw_completion(ErrorType::IntlInvalidTime); } + // 2. Let result be the empty String. + // 3. For each Record { [[Type]], [[Value]] } part in parts, do + // a. Set result to the string-concatenation of result and part.[[Value]]. // 4. Return result. - return MUST(result.to_string()); + return date_time_format.formatter().format(time); } // 11.5.8 FormatDateTimeToParts ( dateTimeFormat, x ), https://tc39.es/ecma402/#sec-formatdatetimetoparts @@ -808,43 +106,8 @@ ThrowCompletionOr> format_date_time_to_parts(VM& vm, DateTim return result; } -template -void for_each_range_pattern_field(LocalTime const& time1, LocalTime const& time2, Callback&& callback) -{ - // Table 4: Range pattern fields, https://tc39.es/ecma402/#table-datetimeformat-rangepatternfields - if (callback(static_cast(time1.era), static_cast(time2.era), ::Locale::CalendarRangePattern::Field::Era) == IterationDecision::Break) - return; - if (callback(time1.year, time2.year, ::Locale::CalendarRangePattern::Field::Year) == IterationDecision::Break) - return; - if (callback(time1.month, time2.month, ::Locale::CalendarRangePattern::Field::Month) == IterationDecision::Break) - return; - if (callback(time1.day, time2.day, ::Locale::CalendarRangePattern::Field::Day) == IterationDecision::Break) - return; - if (callback(time1.hour, time2.hour, ::Locale::CalendarRangePattern::Field::AmPm) == IterationDecision::Break) - return; - if (callback(time1.hour, time2.hour, ::Locale::CalendarRangePattern::Field::DayPeriod) == IterationDecision::Break) - return; - if (callback(time1.hour, time2.hour, ::Locale::CalendarRangePattern::Field::Hour) == IterationDecision::Break) - return; - if (callback(time1.minute, time2.minute, ::Locale::CalendarRangePattern::Field::Minute) == IterationDecision::Break) - return; - if (callback(time1.second, time2.second, ::Locale::CalendarRangePattern::Field::Second) == IterationDecision::Break) - return; - if (callback(time1.millisecond, time2.millisecond, ::Locale::CalendarRangePattern::Field::FractionalSecondDigits) == IterationDecision::Break) - return; -} - -template -static ThrowCompletionOr for_each_range_pattern_with_source(::Locale::CalendarRangePattern& pattern, Callback&& callback) -{ - TRY(callback(pattern.start_range, "startRange"sv)); - TRY(callback(pattern.separator, "shared"sv)); - TRY(callback(pattern.end_range, "endRange"sv)); - return {}; -} - // 11.5.9 PartitionDateTimeRangePattern ( dateTimeFormat, x, y ), https://tc39.es/ecma402/#sec-partitiondatetimerangepattern -ThrowCompletionOr> partition_date_time_range_pattern(VM& vm, DateTimeFormat& date_time_format, double start, double end) +ThrowCompletionOr> partition_date_time_range_pattern(VM& vm, DateTimeFormat& date_time_format, double start, double end) { // 1. Let x be TimeClip(x). start = time_clip(start); @@ -860,232 +123,37 @@ ThrowCompletionOr> partition_date_time_range_ if (isnan(end)) return vm.throw_completion(ErrorType::IntlInvalidTime); - // 5. Let tm1 be ToLocalTime(ℤ(ℝ(x) × 10^6), dateTimeFormat.[[Calendar]], dateTimeFormat.[[TimeZone]]). - auto start_bigint = Crypto::SignedBigInteger { start }.multiplied_by(s_one_million_bigint); - auto start_local_time = TRY(to_local_time(vm, start_bigint, date_time_format.calendar(), date_time_format.time_zone())); - - // 6. Let tm2 be ToLocalTime(ℤ(ℝ(y) × 10^6), dateTimeFormat.[[Calendar]], dateTimeFormat.[[TimeZone]]). - auto end_bigint = Crypto::SignedBigInteger { end }.multiplied_by(s_one_million_bigint); - auto end_local_time = TRY(to_local_time(vm, end_bigint, date_time_format.calendar(), date_time_format.time_zone())); - - // 7. Let rangePatterns be dateTimeFormat.[[RangePatterns]]. - auto range_patterns = date_time_format.range_patterns(); - - // 8. Let rangePattern be undefined. - Optional<::Locale::CalendarRangePattern> range_pattern; - - // 9. Let dateFieldsPracticallyEqual be true. - bool date_fields_practically_equal = true; - - // 10. Let patternContainsLargerDateField be false. - bool pattern_contains_larger_date_field = false; - - // 11. While dateFieldsPracticallyEqual is true and patternContainsLargerDateField is false, repeat for each row of Table 4 in order, except the header row: - for_each_range_pattern_field(start_local_time, end_local_time, [&](auto start_value, auto end_value, auto field_name) { - // a. Let fieldName be the name given in the Range Pattern Field column of the row. - - // b. If rangePatterns has a field [[]], let rp be rangePatterns.[[]]; else let rp be undefined. - Optional<::Locale::CalendarRangePattern> pattern; - for (auto const& range : range_patterns) { - if (range.field == field_name) { - pattern = range; - break; - } - } - - // c. If rangePattern is not undefined and rp is undefined, then - if (range_pattern.has_value() && !pattern.has_value()) { - // i. Set patternContainsLargerDateField to true. - pattern_contains_larger_date_field = true; - } - // d. Else, - else { - // i. Let rangePattern be rp. - range_pattern = pattern; - - switch (field_name) { - // ii. If fieldName is equal to [[AmPm]], then - case ::Locale::CalendarRangePattern::Field::AmPm: { - // 1. Let v1 be tm1.[[Hour]]. - // 2. Let v2 be tm2.[[Hour]]. - - // 3. If v1 is greater than 11 and v2 less or equal than 11, or v1 is less or equal than 11 and v2 is greater than 11, then - if ((start_value > 11 && end_value <= 11) || (start_value <= 11 && end_value > 11)) { - // a. Set dateFieldsPracticallyEqual to false. - date_fields_practically_equal = false; - } - - break; - } - // iii. Else if fieldName is equal to [[DayPeriod]], then - case ::Locale::CalendarRangePattern::Field::DayPeriod: { - // 1. Let v1 be a String value representing the day period of tm1; the String value depends upon the implementation and the effective locale of dateTimeFormat. - auto start_period = ::Locale::get_calendar_day_period_symbol_for_hour(date_time_format.data_locale(), date_time_format.calendar(), ::Locale::CalendarPatternStyle::Short, start_value); - - // 2. Let v2 be a String value representing the day period of tm2; the String value depends upon the implementation and the effective locale of dateTimeFormat. - auto end_period = ::Locale::get_calendar_day_period_symbol_for_hour(date_time_format.data_locale(), date_time_format.calendar(), ::Locale::CalendarPatternStyle::Short, end_value); - - // 3. If v1 is not equal to v2, then - if (start_period != end_period) { - // a. Set dateFieldsPracticallyEqual to false. - date_fields_practically_equal = false; - } - - break; - } - // iv. Else if fieldName is equal to [[FractionalSecondDigits]], then - case ::Locale::CalendarRangePattern::Field::FractionalSecondDigits: { - // 1. Let fractionalSecondDigits be dateTimeFormat.[[FractionalSecondDigits]]. - Optional fractional_second_digits; - if (date_time_format.has_fractional_second_digits()) - fractional_second_digits = date_time_format.fractional_second_digits(); - - // 2. If fractionalSecondDigits is undefined, then - if (!fractional_second_digits.has_value()) { - // a. Set fractionalSecondDigits to 3. - fractional_second_digits = 3; - } - - // 3. Let v1 be tm1.[[Millisecond]]. - // 4. Let v2 be tm2.[[Millisecond]]. - - // 5. Let v1 be floor(v1 × 10( fractionalSecondDigits - 3 )). - start_value = floor(start_value * pow(10, static_cast(*fractional_second_digits) - 3)); - - // 6. Let v2 be floor(v2 × 10( fractionalSecondDigits - 3 )). - end_value = floor(end_value * pow(10, static_cast(*fractional_second_digits) - 3)); - - // 7. If v1 is not equal to v2, then - if (start_value != end_value) { - // a. Set dateFieldsPracticallyEqual to false. - date_fields_practically_equal = false; - } - - break; - } - - // v. Else, - default: { - // 1. Let v1 be tm1.[[]]. - // 2. Let v2 be tm2.[[]]. - - // 3. If v1 is not equal to v2, then - if (start_value != end_value) { - // a. Set dateFieldsPracticallyEqual to false. - date_fields_practically_equal = false; - } - - break; - } - } - } - - if (date_fields_practically_equal && !pattern_contains_larger_date_field) - return IterationDecision::Continue; - return IterationDecision::Break; - }); - - // 12. If dateFieldsPracticallyEqual is true, then - if (date_fields_practically_equal) { - // a. Let pattern be dateTimeFormat.[[Pattern]]. - auto const& pattern = date_time_format.pattern(); - - // b. Let patternParts be PartitionPattern(pattern). - auto pattern_parts = partition_pattern(pattern); - - // c. Let result be ? FormatDateTimePattern(dateTimeFormat, patternParts, x, undefined). - auto raw_result = TRY(format_date_time_pattern(vm, date_time_format, move(pattern_parts), start, nullptr)); - auto result = PatternPartitionWithSource::create_from_parent_list(move(raw_result)); - - // d. For each Record { [[Type]], [[Value]] } r in result, do - for (auto& part : result) { - // i. Set r.[[Source]] to "shared". - part.source = "shared"sv; - } - - // e. Return result. - return result; - } - - // 13. Let result be a new empty List. - Vector result; - - // 14. If rangePattern is undefined, then - if (!range_pattern.has_value()) { - // a. Let rangePattern be rangePatterns.[[Default]]. - range_pattern = ::Locale::get_calendar_default_range_format(date_time_format.data_locale(), date_time_format.calendar()); - - // Non-standard, range_pattern will be empty if Unicode data generation is disabled. - if (!range_pattern.has_value()) - return result; - - // Non-standard, LibUnicode leaves the CLDR's {0} and {1} partitions in the default patterns - // to be replaced at runtime with the DateTimeFormat object's pattern. - auto const& pattern = date_time_format.pattern(); - - if (range_pattern->start_range.contains("{0}"sv)) { - range_pattern->start_range = MUST(range_pattern->start_range.replace("{0}"sv, pattern, ReplaceMode::FirstOnly)); - range_pattern->end_range = MUST(range_pattern->end_range.replace("{1}"sv, pattern, ReplaceMode::FirstOnly)); - } else { - range_pattern->start_range = MUST(range_pattern->start_range.replace("{1}"sv, pattern, ReplaceMode::FirstOnly)); - range_pattern->end_range = MUST(range_pattern->end_range.replace("{0}"sv, pattern, ReplaceMode::FirstOnly)); - } - - // FIXME: The above is not sufficient. For example, if the start date is days before the end date, and only the timeStyle - // option is provided, the resulting range will not include the differing dates. We will likely need to implement - // step 3 here: https://unicode.org/reports/tr35/tr35-dates.html#intervalFormats - } - - // 15. For each Record { [[Pattern]], [[Source]] } rangePatternPart in rangePattern.[[PatternParts]], do - TRY(for_each_range_pattern_with_source(*range_pattern, [&](auto const& pattern, auto source) -> ThrowCompletionOr { - // a. Let pattern be rangePatternPart.[[Pattern]]. - // b. Let source be rangePatternPart.[[Source]]. - - // c. If source is "startRange" or "shared", then - // i. Let z be x. - // d. Else, - // i. Let z be y. - auto time = ((source == "startRange") || (source == "shared")) ? start : end; - - // e. Let patternParts be PartitionPattern(pattern). - auto pattern_parts = partition_pattern(pattern); - - // f. Let partResult be ? FormatDateTimePattern(dateTimeFormat, patternParts, z, rangePattern). - auto raw_part_result = TRY(format_date_time_pattern(vm, date_time_format, move(pattern_parts), time, &range_pattern.value())); - auto part_result = PatternPartitionWithSource::create_from_parent_list(move(raw_part_result)); - - // g. For each Record { [[Type]], [[Value]] } r in partResult, do - for (auto& part : part_result) { - // i. Set r.[[Source]] to source. - part.source = source; - } - - // h. Add all elements in partResult to result in order. - result.extend(move(part_result)); - return {}; - })); - - // 16. Return result. - return result; + return date_time_format.formatter().format_range_to_parts(start, end); } // 11.5.10 FormatDateTimeRange ( dateTimeFormat, x, y ), https://tc39.es/ecma402/#sec-formatdatetimerange ThrowCompletionOr format_date_time_range(VM& vm, DateTimeFormat& date_time_format, double start, double end) { - // 1. Let parts be ? PartitionDateTimeRangePattern(dateTimeFormat, x, y). - auto parts = TRY(partition_date_time_range_pattern(vm, date_time_format, start, end)); + { + // NOTE: We short-circuit PartitionDateTimeRangePattern as we do not need individual partitions. But we must + // still perform the time clip and NaN sanity checks from its its first steps. - // 2. Let result be the empty String. - StringBuilder result; + // 1. Let x be TimeClip(x). + start = time_clip(start); - // 3. For each Record { [[Type]], [[Value]], [[Source]] } part in parts, do - for (auto& part : parts) { - // a. Set result to the string-concatenation of result and part.[[Value]]. - result.append(part.value); + // 2. If x is NaN, throw a RangeError exception. + if (isnan(start)) + return vm.throw_completion(ErrorType::IntlInvalidTime); + + // 3. Let y be TimeClip(y). + end = time_clip(end); + + // 4. If y is NaN, throw a RangeError exception. + if (isnan(end)) + return vm.throw_completion(ErrorType::IntlInvalidTime); } + // 1. Let parts be ? PartitionDateTimeRangePattern(dateTimeFormat, x, y). + // 2. Let result be the empty String. + // 3. For each Record { [[Type]], [[Value]], [[Source]] } part in parts, do + // a. Set result to the string-concatenation of result and part.[[Value]]. // 4. Return result. - return MUST(result.to_string()); + return date_time_format.formatter().format_range(start, end); } // 11.5.11 FormatDateTimeRangeToParts ( dateTimeFormat, x, y ), https://tc39.es/ecma402/#sec-formatdatetimerangetoparts @@ -1127,70 +195,4 @@ ThrowCompletionOr> format_date_time_range_to_parts(VM& vm, D return result; } -// 11.5.12 ToLocalTime ( epochNs, calendar, timeZoneIdentifier ), https://tc39.es/ecma402/#sec-tolocaltime -ThrowCompletionOr to_local_time(VM& vm, Crypto::SignedBigInteger const& epoch_ns, StringView calendar, StringView time_zone_identifier) -{ - double offset_ns { 0 }; - - // 1. If IsTimeZoneOffsetString(timeZoneIdentifier) is true, then - if (is_time_zone_offset_string(time_zone_identifier)) { - // a. Let offsetNs be ParseTimeZoneOffsetString(timeZoneIdentifier). - offset_ns = parse_time_zone_offset_string(time_zone_identifier); - } - // 2. Else, - else { - // a. Assert: IsValidTimeZoneName(timeZoneIdentifier) is true. - VERIFY(Temporal::is_available_time_zone_name(time_zone_identifier)); - - // b. Let offsetNs be GetNamedTimeZoneOffsetNanoseconds(timeZoneIdentifier, epochNs). - offset_ns = get_named_time_zone_offset_nanoseconds(time_zone_identifier, epoch_ns); - } - - // NOTE: Unlike the spec, we still perform the below computations with BigInts until we are ready - // to divide the number by 10^6. The spec expects an MV here. If we try to use i64, we will - // overflow; if we try to use a double, we lose quite a bit of accuracy. - - // 3. Let tz be ℝ(epochNs) + offsetNs. - auto zoned_time_ns = epoch_ns.plus(Crypto::SignedBigInteger { offset_ns }); - - // 4. If calendar is "gregory", then - if (calendar == "gregory"sv) { - auto zoned_time_ms = zoned_time_ns.divided_by(s_one_million_bigint).quotient; - auto zoned_time = floor(zoned_time_ms.to_double(Crypto::UnsignedBigInteger::RoundingMode::ECMAScriptNumberValueFor)); - - auto year = year_from_time(zoned_time); - - // a. Return a record with fields calculated from tz according to Table 8. - return LocalTime { - // WeekDay(𝔽(floor(tz / 10^6))) - .weekday = week_day(zoned_time), - // Let year be YearFromTime(𝔽(floor(tz / 10^6))). If year < 1𝔽, return "BC", else return "AD". - .era = year < 1 ? ::Locale::Era::BC : ::Locale::Era::AD, - // YearFromTime(𝔽(floor(tz / 10^6))) - .year = year, - // undefined. - .related_year = js_undefined(), - // undefined. - .year_name = js_undefined(), - // MonthFromTime(𝔽(floor(tz / 10^6))) - .month = month_from_time(zoned_time), - // DateFromTime(𝔽(floor(tz / 10^6))) - .day = date_from_time(zoned_time), - // HourFromTime(𝔽(floor(tz / 10^6))) - .hour = hour_from_time(zoned_time), - // MinFromTime(𝔽(floor(tz / 10^6))) - .minute = min_from_time(zoned_time), - // SecFromTime(𝔽(floor(tz / 10^6))) - .second = sec_from_time(zoned_time), - // msFromTime(𝔽(floor(tz / 10^6))) - .millisecond = ms_from_time(zoned_time), - }; - } - - // 5. Else, - // a. Return a record with the fields of Column 1 of Table 8 calculated from tz for the given calendar. The calculations should use best available information about the specified calendar. - // FIXME: Implement this when non-Gregorian calendars are supported by LibUnicode. - return vm.throw_completion(ErrorType::NotImplemented, "Non-Gregorian calendars"sv); -} - } diff --git a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.h b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.h index 395cab19dbb..c4c723fa7fb 100644 --- a/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.h +++ b/Userland/Libraries/LibJS/Runtime/Intl/DateTimeFormat.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2023, Tim Flynn + * Copyright (c) 2021-2024, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ @@ -9,12 +9,11 @@ #include #include #include -#include #include #include #include -#include #include +#include #include namespace JS::Intl { @@ -28,13 +27,6 @@ class DateTimeFormat final using Patterns = ::Locale::CalendarPattern; public: - enum class Style { - Full, - Long, - Medium, - Short, - }; - static constexpr auto relevant_extension_keys() { // 11.2.3 Internal slots, https://tc39.es/ecma402/#sec-intl.datetimeformat-internal-slots @@ -56,130 +48,51 @@ public: String const& numbering_system() const { return m_numbering_system; } void set_numbering_system(String numbering_system) { m_numbering_system = move(numbering_system); } - bool has_hour_cycle() const { return m_hour_cycle.has_value(); } - ::Locale::HourCycle hour_cycle() const { return *m_hour_cycle; } - StringView hour_cycle_string() const { return ::Locale::hour_cycle_to_string(*m_hour_cycle); } - void set_hour_cycle(::Locale::HourCycle hour_cycle) { m_hour_cycle = hour_cycle; } - void clear_hour_cycle() { m_hour_cycle.clear(); } - String const& time_zone() const { return m_time_zone; } void set_time_zone(String time_zone) { m_time_zone = move(time_zone); } bool has_date_style() const { return m_date_style.has_value(); } - Style date_style() const { return *m_date_style; } - StringView date_style_string() const { return style_to_string(*m_date_style); } - void set_date_style(StringView style) { m_date_style = style_from_string(style); } + Optional<::Locale::DateTimeStyle> const& date_style() const { return m_date_style; } + StringView date_style_string() const { return ::Locale::date_time_style_to_string(*m_date_style); } + void set_date_style(StringView style) { m_date_style = ::Locale::date_time_style_from_string(style); } bool has_time_style() const { return m_time_style.has_value(); } - Style time_style() const { return *m_time_style; } - StringView time_style_string() const { return style_to_string(*m_time_style); } - void set_time_style(StringView style) { m_time_style = style_from_string(style); } - - String const& pattern() const { return Patterns::pattern; } - void set_pattern(String pattern) { Patterns::pattern = move(pattern); } - - ReadonlySpan<::Locale::CalendarRangePattern> range_patterns() const { return m_range_patterns.span(); } - void set_range_patterns(Vector<::Locale::CalendarRangePattern> range_patterns) { m_range_patterns = move(range_patterns); } - - bool has_era() const { return Patterns::era.has_value(); } - ::Locale::CalendarPatternStyle era() const { return *Patterns::era; } - StringView era_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::era); } - - bool has_year() const { return Patterns::year.has_value(); } - ::Locale::CalendarPatternStyle year() const { return *Patterns::year; } - StringView year_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::year); } - - bool has_month() const { return Patterns::month.has_value(); } - ::Locale::CalendarPatternStyle month() const { return *Patterns::month; } - StringView month_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::month); } - - bool has_weekday() const { return Patterns::weekday.has_value(); } - ::Locale::CalendarPatternStyle weekday() const { return *Patterns::weekday; } - StringView weekday_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::weekday); } - - bool has_day() const { return Patterns::day.has_value(); } - ::Locale::CalendarPatternStyle day() const { return *Patterns::day; } - StringView day_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::day); } - - bool has_day_period() const { return Patterns::day_period.has_value(); } - ::Locale::CalendarPatternStyle day_period() const { return *Patterns::day_period; } - StringView day_period_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::day_period); } - - bool has_hour() const { return Patterns::hour.has_value(); } - ::Locale::CalendarPatternStyle hour() const { return *Patterns::hour; } - StringView hour_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::hour); } - - bool has_minute() const { return Patterns::minute.has_value(); } - ::Locale::CalendarPatternStyle minute() const { return *Patterns::minute; } - StringView minute_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::minute); } - - bool has_second() const { return Patterns::second.has_value(); } - ::Locale::CalendarPatternStyle second() const { return *Patterns::second; } - StringView second_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::second); } - - bool has_fractional_second_digits() const { return Patterns::fractional_second_digits.has_value(); } - u8 fractional_second_digits() const { return *Patterns::fractional_second_digits; } - - bool has_time_zone_name() const { return Patterns::time_zone_name.has_value(); } - ::Locale::CalendarPatternStyle time_zone_name() const { return *Patterns::time_zone_name; } - StringView time_zone_name_string() const { return ::Locale::calendar_pattern_style_to_string(*Patterns::time_zone_name); } + Optional<::Locale::DateTimeStyle> const& time_style() const { return m_time_style; } + StringView time_style_string() const { return ::Locale::date_time_style_to_string(*m_time_style); } + void set_time_style(StringView style) { m_time_style = ::Locale::date_time_style_from_string(style); } NativeFunction* bound_format() const { return m_bound_format; } void set_bound_format(NativeFunction* bound_format) { m_bound_format = bound_format; } + ::Locale::DateTimeFormat const& formatter() const { return *m_formatter; } + void set_formatter(NonnullOwnPtr<::Locale::DateTimeFormat> formatter) { m_formatter = move(formatter); } + private: DateTimeFormat(Object& prototype); - static Style style_from_string(StringView style); - static StringView style_to_string(Style style); - virtual void visit_edges(Visitor&) override; - String m_locale; // [[Locale]] - String m_calendar; // [[Calendar]] - String m_numbering_system; // [[NumberingSystem]] - Optional<::Locale::HourCycle> m_hour_cycle; // [[HourCycle]] - String m_time_zone; // [[TimeZone]] - Optional