/* * Copyright (c) 2021-2023, Tim Flynn * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include namespace Locale { HourCycle hour_cycle_from_string(StringView hour_cycle) { if (hour_cycle == "h11"sv) return HourCycle::H11; if (hour_cycle == "h12"sv) return HourCycle::H12; if (hour_cycle == "h23"sv) return HourCycle::H23; if (hour_cycle == "h24"sv) return HourCycle::H24; VERIFY_NOT_REACHED(); } StringView hour_cycle_to_string(HourCycle hour_cycle) { switch (hour_cycle) { case HourCycle::H11: return "h11"sv; case HourCycle::H12: return "h12"sv; case HourCycle::H23: return "h23"sv; case HourCycle::H24: return "h24"sv; default: VERIFY_NOT_REACHED(); } } CalendarPatternStyle calendar_pattern_style_from_string(StringView style) { if (style == "narrow"sv) return CalendarPatternStyle::Narrow; if (style == "short"sv) return CalendarPatternStyle::Short; if (style == "long"sv) return CalendarPatternStyle::Long; if (style == "numeric"sv) return CalendarPatternStyle::Numeric; if (style == "2-digit"sv) return CalendarPatternStyle::TwoDigit; if (style == "shortOffset"sv) return CalendarPatternStyle::ShortOffset; if (style == "longOffset"sv) return CalendarPatternStyle::LongOffset; if (style == "shortGeneric"sv) return CalendarPatternStyle::ShortGeneric; if (style == "longGeneric"sv) return CalendarPatternStyle::LongGeneric; VERIFY_NOT_REACHED(); } StringView calendar_pattern_style_to_string(CalendarPatternStyle style) { switch (style) { case CalendarPatternStyle::Narrow: return "narrow"sv; case CalendarPatternStyle::Short: return "short"sv; case CalendarPatternStyle::Long: return "long"sv; case CalendarPatternStyle::Numeric: return "numeric"sv; case CalendarPatternStyle::TwoDigit: return "2-digit"sv; case CalendarPatternStyle::ShortOffset: return "shortOffset"sv; case CalendarPatternStyle::LongOffset: return "longOffset"sv; case CalendarPatternStyle::ShortGeneric: return "shortGeneric"sv; case CalendarPatternStyle::LongGeneric: return "longGeneric"sv; default: VERIFY_NOT_REACHED(); } } Optional __attribute__((weak)) hour_cycle_region_from_string(StringView) { return {}; } ErrorOr> __attribute__((weak)) get_regional_hour_cycles(StringView) { return Vector {}; } template GetRegionalValues> static ErrorOr find_regional_values_for_locale(StringView locale, GetRegionalValues&& get_regional_values) { auto has_value = [](auto const& container) { if constexpr (requires { container.has_value(); }) return container.has_value(); else return !container.is_empty(); }; if (auto regional_values = TRY(get_regional_values(locale)); has_value(regional_values)) return regional_values; auto return_default_values = [&]() { return get_regional_values("001"sv); }; auto language = TRY(parse_unicode_language_id(locale)); if (!language.has_value()) return return_default_values(); if (!language->region.has_value()) language = TRY(add_likely_subtags(*language)); if (!language.has_value() || !language->region.has_value()) return return_default_values(); if (auto regional_values = TRY(get_regional_values(*language->region)); has_value(regional_values)) return regional_values; return return_default_values(); } template static ErrorOr find_regional_values_for_locale(StringView locale, GetRegionalValues&& get_regional_values) { return find_regional_values_for_locale(locale, [&](auto region) -> ErrorOr { return get_regional_values(region); }); } // https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table ErrorOr> get_locale_hour_cycles(StringView locale) { return find_regional_values_for_locale>(locale, get_regional_hour_cycles); } ErrorOr> get_default_regional_hour_cycle(StringView locale) { if (auto hour_cycles = TRY(get_locale_hour_cycles(locale)); !hour_cycles.is_empty()) return hour_cycles.first(); return OptionalNone {}; } Optional __attribute__((weak)) minimum_days_region_from_string(StringView) { return {}; } Optional __attribute__((weak)) get_regional_minimum_days(StringView) { return {}; } ErrorOr> get_locale_minimum_days(StringView locale) { return find_regional_values_for_locale>(locale, get_regional_minimum_days); } Optional __attribute__((weak)) first_day_region_from_string(StringView) { return {}; } Optional __attribute__((weak)) get_regional_first_day(StringView) { return {}; } ErrorOr> get_locale_first_day(StringView locale) { return find_regional_values_for_locale>(locale, get_regional_first_day); } Optional __attribute__((weak)) weekend_start_region_from_string(StringView) { return {}; } Optional __attribute__((weak)) get_regional_weekend_start(StringView) { return {}; } ErrorOr> get_locale_weekend_start(StringView locale) { return find_regional_values_for_locale>(locale, get_regional_weekend_start); } Optional __attribute__((weak)) weekend_end_region_from_string(StringView) { return {}; } Optional __attribute__((weak)) get_regional_weekend_end(StringView) { return {}; } ErrorOr> get_locale_weekend_end(StringView locale) { return find_regional_values_for_locale>(locale, get_regional_weekend_end); } ErrorOr combine_skeletons(StringView first, StringView second) { // https://unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems constexpr auto field_order = Array { "G"sv, // Era "yYuUr"sv, // Year "ML"sv, // Month "dDFg"sv, // Day "Eec"sv, // Weekday "abB"sv, // Period "hHKk"sv, // Hour "m"sv, // Minute "sSA"sv, // Second "zZOvVXx"sv, // Zone }; StringBuilder builder; auto append_from_skeleton = [&](auto skeleton, auto ch) -> ErrorOr { auto first_index = skeleton.find(ch); if (!first_index.has_value()) return false; auto last_index = skeleton.find_last(ch); TRY(builder.try_append(skeleton.substring_view(*first_index, *last_index - *first_index + 1))); return true; }; for (auto fields : field_order) { for (auto ch : fields) { if (TRY(append_from_skeleton(first, ch))) break; if (TRY(append_from_skeleton(second, ch))) break; } } return builder.to_string(); } ErrorOr> __attribute__((weak)) get_calendar_date_format(StringView, StringView) { return OptionalNone {}; } ErrorOr> __attribute__((weak)) get_calendar_time_format(StringView, StringView) { return OptionalNone {}; } ErrorOr> __attribute__((weak)) get_calendar_date_time_format(StringView, StringView) { return OptionalNone {}; } ErrorOr> get_calendar_format(StringView locale, StringView calendar, CalendarFormatType type) { switch (type) { case CalendarFormatType::Date: return get_calendar_date_format(locale, calendar); case CalendarFormatType::Time: return get_calendar_time_format(locale, calendar); case CalendarFormatType::DateTime: return get_calendar_date_time_format(locale, calendar); default: VERIFY_NOT_REACHED(); } } ErrorOr> __attribute__((weak)) get_calendar_available_formats(StringView, StringView) { return Vector {}; } ErrorOr> __attribute__((weak)) get_calendar_default_range_format(StringView, StringView) { return OptionalNone {}; } ErrorOr> __attribute__((weak)) get_calendar_range_formats(StringView, StringView, StringView) { return Vector {}; } ErrorOr> __attribute__((weak)) get_calendar_range12_formats(StringView, StringView, StringView) { return Vector {}; } ErrorOr> __attribute__((weak)) get_calendar_era_symbol(StringView, StringView, CalendarPatternStyle, Era) { return OptionalNone {}; } ErrorOr> __attribute__((weak)) get_calendar_month_symbol(StringView, StringView, CalendarPatternStyle, Month) { return OptionalNone {}; } ErrorOr> __attribute__((weak)) get_calendar_weekday_symbol(StringView, StringView, CalendarPatternStyle, Weekday) { return OptionalNone {}; } ErrorOr> __attribute__((weak)) get_calendar_day_period_symbol(StringView, StringView, CalendarPatternStyle, DayPeriod) { return OptionalNone {}; } ErrorOr> __attribute__((weak)) get_calendar_day_period_symbol_for_hour(StringView, StringView, CalendarPatternStyle, u8) { return OptionalNone {}; } Optional __attribute__((weak)) get_time_zone_name(StringView, StringView, CalendarPatternStyle, TimeZone::InDST) { return {}; } Optional __attribute__((weak)) get_time_zone_format(StringView) { return {}; } static ErrorOr> format_time_zone_offset(StringView locale, CalendarPatternStyle style, i64 offset_seconds) { auto formats = get_time_zone_format(locale); if (!formats.has_value()) return OptionalNone {}; auto number_system = TRY(get_preferred_keyword_value_for_locale(locale, "nu"sv)); if (!number_system.has_value()) return OptionalNone {}; if (offset_seconds == 0) return String::from_utf8(formats->gmt_zero_format); auto sign = offset_seconds > 0 ? formats->symbol_ahead_sign : formats->symbol_behind_sign; auto separator = offset_seconds > 0 ? formats->symbol_ahead_separator : formats->symbol_behind_separator; offset_seconds = llabs(offset_seconds); auto offset_hours = offset_seconds / 3'600; offset_seconds %= 3'600; auto offset_minutes = offset_seconds / 60; offset_seconds %= 60; StringBuilder builder; TRY(builder.try_append(sign)); switch (style) { // The long format always uses 2-digit hours field and minutes field, with optional 2-digit seconds field. case CalendarPatternStyle::LongOffset: TRY(builder.try_appendff("{:02}{}{:02}", offset_hours, separator, offset_minutes)); if (offset_seconds > 0) TRY(builder.try_appendff("{}{:02}", separator, offset_seconds)); break; // The short format is intended for the shortest representation and uses hour fields without leading zero, with optional 2-digit minutes and seconds fields. case CalendarPatternStyle::ShortOffset: TRY(builder.try_appendff("{}", offset_hours)); if (offset_minutes > 0) { TRY(builder.try_appendff("{}{:02}", separator, offset_minutes)); if (offset_seconds > 0) TRY(builder.try_appendff("{}{:02}", separator, offset_seconds)); } break; default: VERIFY_NOT_REACHED(); } // The digits used for hours, minutes and seconds fields in this format are the locale's default decimal digits. auto result = TRY(replace_digits_for_number_system(*number_system, TRY(builder.to_string()))); return TRY(String::from_utf8(formats->gmt_format)).replace("{0}"sv, result, ReplaceMode::FirstOnly); } // https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology ErrorOr format_time_zone(StringView locale, StringView time_zone, CalendarPatternStyle style, AK::UnixDateTime time) { auto offset = TimeZone::get_time_zone_offset(time_zone, time); if (!offset.has_value()) return String::from_utf8(time_zone); switch (style) { case CalendarPatternStyle::Short: case CalendarPatternStyle::Long: case CalendarPatternStyle::ShortGeneric: case CalendarPatternStyle::LongGeneric: if (auto name = get_time_zone_name(locale, time_zone, style, offset->in_dst); name.has_value()) return String::from_utf8(*name); break; case CalendarPatternStyle::ShortOffset: case CalendarPatternStyle::LongOffset: if (auto formatted_offset = TRY(format_time_zone_offset(locale, style, offset->seconds)); formatted_offset.has_value()) return formatted_offset.release_value(); return String::from_utf8(time_zone); default: VERIFY_NOT_REACHED(); } // If more styles are added, consult the following table to ensure always falling back to GMT offset is still correct: // https://unicode.org/reports/tr35/tr35-dates.html#dfst-zone switch (style) { case CalendarPatternStyle::Short: case CalendarPatternStyle::ShortGeneric: return format_time_zone(locale, time_zone, CalendarPatternStyle::ShortOffset, time); case CalendarPatternStyle::Long: case CalendarPatternStyle::LongGeneric: return format_time_zone(locale, time_zone, CalendarPatternStyle::LongOffset, time); default: VERIFY_NOT_REACHED(); } } }