Serenity Operating System
at master 345 lines 14 kB view raw
1/* 2 * Copyright (c) 2021-2023, Tim Flynn <trflynn89@serenityos.org> 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <AK/Array.h> 8#include <AK/StringBuilder.h> 9#include <LibLocale/DateTimeFormat.h> 10#include <LibLocale/Locale.h> 11#include <LibLocale/NumberFormat.h> 12#include <stdlib.h> 13 14namespace Locale { 15 16HourCycle hour_cycle_from_string(StringView hour_cycle) 17{ 18 if (hour_cycle == "h11"sv) 19 return HourCycle::H11; 20 if (hour_cycle == "h12"sv) 21 return HourCycle::H12; 22 if (hour_cycle == "h23"sv) 23 return HourCycle::H23; 24 if (hour_cycle == "h24"sv) 25 return HourCycle::H24; 26 VERIFY_NOT_REACHED(); 27} 28 29StringView hour_cycle_to_string(HourCycle hour_cycle) 30{ 31 switch (hour_cycle) { 32 case HourCycle::H11: 33 return "h11"sv; 34 case HourCycle::H12: 35 return "h12"sv; 36 case HourCycle::H23: 37 return "h23"sv; 38 case HourCycle::H24: 39 return "h24"sv; 40 default: 41 VERIFY_NOT_REACHED(); 42 } 43} 44 45CalendarPatternStyle calendar_pattern_style_from_string(StringView style) 46{ 47 if (style == "narrow"sv) 48 return CalendarPatternStyle::Narrow; 49 if (style == "short"sv) 50 return CalendarPatternStyle::Short; 51 if (style == "long"sv) 52 return CalendarPatternStyle::Long; 53 if (style == "numeric"sv) 54 return CalendarPatternStyle::Numeric; 55 if (style == "2-digit"sv) 56 return CalendarPatternStyle::TwoDigit; 57 if (style == "shortOffset"sv) 58 return CalendarPatternStyle::ShortOffset; 59 if (style == "longOffset"sv) 60 return CalendarPatternStyle::LongOffset; 61 if (style == "shortGeneric"sv) 62 return CalendarPatternStyle::ShortGeneric; 63 if (style == "longGeneric"sv) 64 return CalendarPatternStyle::LongGeneric; 65 VERIFY_NOT_REACHED(); 66} 67 68StringView calendar_pattern_style_to_string(CalendarPatternStyle style) 69{ 70 switch (style) { 71 case CalendarPatternStyle::Narrow: 72 return "narrow"sv; 73 case CalendarPatternStyle::Short: 74 return "short"sv; 75 case CalendarPatternStyle::Long: 76 return "long"sv; 77 case CalendarPatternStyle::Numeric: 78 return "numeric"sv; 79 case CalendarPatternStyle::TwoDigit: 80 return "2-digit"sv; 81 case CalendarPatternStyle::ShortOffset: 82 return "shortOffset"sv; 83 case CalendarPatternStyle::LongOffset: 84 return "longOffset"sv; 85 case CalendarPatternStyle::ShortGeneric: 86 return "shortGeneric"sv; 87 case CalendarPatternStyle::LongGeneric: 88 return "longGeneric"sv; 89 default: 90 VERIFY_NOT_REACHED(); 91 } 92} 93 94Optional<HourCycleRegion> __attribute__((weak)) hour_cycle_region_from_string(StringView) { return {}; } 95ErrorOr<Vector<HourCycle>> __attribute__((weak)) get_regional_hour_cycles(StringView) { return Vector<HourCycle> {}; } 96 97template<typename T, FallibleFunction<StringView> GetRegionalValues> 98static ErrorOr<T> find_regional_values_for_locale(StringView locale, GetRegionalValues&& get_regional_values) 99{ 100 auto has_value = [](auto const& container) { 101 if constexpr (requires { container.has_value(); }) 102 return container.has_value(); 103 else 104 return !container.is_empty(); 105 }; 106 107 if (auto regional_values = TRY(get_regional_values(locale)); has_value(regional_values)) 108 return regional_values; 109 110 auto return_default_values = [&]() { return get_regional_values("001"sv); }; 111 112 auto language = TRY(parse_unicode_language_id(locale)); 113 if (!language.has_value()) 114 return return_default_values(); 115 116 if (!language->region.has_value()) 117 language = TRY(add_likely_subtags(*language)); 118 if (!language.has_value() || !language->region.has_value()) 119 return return_default_values(); 120 121 if (auto regional_values = TRY(get_regional_values(*language->region)); has_value(regional_values)) 122 return regional_values; 123 124 return return_default_values(); 125} 126 127template<typename T, typename GetRegionalValues> 128static ErrorOr<T> find_regional_values_for_locale(StringView locale, GetRegionalValues&& get_regional_values) 129{ 130 return find_regional_values_for_locale<T>(locale, [&](auto region) -> ErrorOr<T> { return get_regional_values(region); }); 131} 132 133// https://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table 134ErrorOr<Vector<HourCycle>> get_locale_hour_cycles(StringView locale) 135{ 136 return find_regional_values_for_locale<Vector<HourCycle>>(locale, get_regional_hour_cycles); 137} 138 139ErrorOr<Optional<HourCycle>> get_default_regional_hour_cycle(StringView locale) 140{ 141 if (auto hour_cycles = TRY(get_locale_hour_cycles(locale)); !hour_cycles.is_empty()) 142 return hour_cycles.first(); 143 return OptionalNone {}; 144} 145 146Optional<MinimumDaysRegion> __attribute__((weak)) minimum_days_region_from_string(StringView) { return {}; } 147Optional<u8> __attribute__((weak)) get_regional_minimum_days(StringView) { return {}; } 148 149ErrorOr<Optional<u8>> get_locale_minimum_days(StringView locale) 150{ 151 return find_regional_values_for_locale<Optional<u8>>(locale, get_regional_minimum_days); 152} 153 154Optional<FirstDayRegion> __attribute__((weak)) first_day_region_from_string(StringView) { return {}; } 155Optional<Weekday> __attribute__((weak)) get_regional_first_day(StringView) { return {}; } 156 157ErrorOr<Optional<Weekday>> get_locale_first_day(StringView locale) 158{ 159 return find_regional_values_for_locale<Optional<Weekday>>(locale, get_regional_first_day); 160} 161 162Optional<WeekendStartRegion> __attribute__((weak)) weekend_start_region_from_string(StringView) { return {}; } 163Optional<Weekday> __attribute__((weak)) get_regional_weekend_start(StringView) { return {}; } 164 165ErrorOr<Optional<Weekday>> get_locale_weekend_start(StringView locale) 166{ 167 return find_regional_values_for_locale<Optional<Weekday>>(locale, get_regional_weekend_start); 168} 169 170Optional<WeekendEndRegion> __attribute__((weak)) weekend_end_region_from_string(StringView) { return {}; } 171Optional<Weekday> __attribute__((weak)) get_regional_weekend_end(StringView) { return {}; } 172 173ErrorOr<Optional<Weekday>> get_locale_weekend_end(StringView locale) 174{ 175 return find_regional_values_for_locale<Optional<Weekday>>(locale, get_regional_weekend_end); 176} 177 178ErrorOr<String> combine_skeletons(StringView first, StringView second) 179{ 180 // https://unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems 181 constexpr auto field_order = Array { 182 "G"sv, // Era 183 "yYuUr"sv, // Year 184 "ML"sv, // Month 185 "dDFg"sv, // Day 186 "Eec"sv, // Weekday 187 "abB"sv, // Period 188 "hHKk"sv, // Hour 189 "m"sv, // Minute 190 "sSA"sv, // Second 191 "zZOvVXx"sv, // Zone 192 }; 193 194 StringBuilder builder; 195 196 auto append_from_skeleton = [&](auto skeleton, auto ch) -> ErrorOr<bool> { 197 auto first_index = skeleton.find(ch); 198 if (!first_index.has_value()) 199 return false; 200 201 auto last_index = skeleton.find_last(ch); 202 203 TRY(builder.try_append(skeleton.substring_view(*first_index, *last_index - *first_index + 1))); 204 return true; 205 }; 206 207 for (auto fields : field_order) { 208 for (auto ch : fields) { 209 if (TRY(append_from_skeleton(first, ch))) 210 break; 211 if (TRY(append_from_skeleton(second, ch))) 212 break; 213 } 214 } 215 216 return builder.to_string(); 217} 218 219ErrorOr<Optional<CalendarFormat>> __attribute__((weak)) get_calendar_date_format(StringView, StringView) { return OptionalNone {}; } 220ErrorOr<Optional<CalendarFormat>> __attribute__((weak)) get_calendar_time_format(StringView, StringView) { return OptionalNone {}; } 221ErrorOr<Optional<CalendarFormat>> __attribute__((weak)) get_calendar_date_time_format(StringView, StringView) { return OptionalNone {}; } 222 223ErrorOr<Optional<CalendarFormat>> get_calendar_format(StringView locale, StringView calendar, CalendarFormatType type) 224{ 225 switch (type) { 226 case CalendarFormatType::Date: 227 return get_calendar_date_format(locale, calendar); 228 case CalendarFormatType::Time: 229 return get_calendar_time_format(locale, calendar); 230 case CalendarFormatType::DateTime: 231 return get_calendar_date_time_format(locale, calendar); 232 default: 233 VERIFY_NOT_REACHED(); 234 } 235} 236 237ErrorOr<Vector<CalendarPattern>> __attribute__((weak)) get_calendar_available_formats(StringView, StringView) { return Vector<CalendarPattern> {}; } 238ErrorOr<Optional<CalendarRangePattern>> __attribute__((weak)) get_calendar_default_range_format(StringView, StringView) { return OptionalNone {}; } 239ErrorOr<Vector<CalendarRangePattern>> __attribute__((weak)) get_calendar_range_formats(StringView, StringView, StringView) { return Vector<CalendarRangePattern> {}; } 240ErrorOr<Vector<CalendarRangePattern>> __attribute__((weak)) get_calendar_range12_formats(StringView, StringView, StringView) { return Vector<CalendarRangePattern> {}; } 241ErrorOr<Optional<StringView>> __attribute__((weak)) get_calendar_era_symbol(StringView, StringView, CalendarPatternStyle, Era) { return OptionalNone {}; } 242ErrorOr<Optional<StringView>> __attribute__((weak)) get_calendar_month_symbol(StringView, StringView, CalendarPatternStyle, Month) { return OptionalNone {}; } 243ErrorOr<Optional<StringView>> __attribute__((weak)) get_calendar_weekday_symbol(StringView, StringView, CalendarPatternStyle, Weekday) { return OptionalNone {}; } 244ErrorOr<Optional<StringView>> __attribute__((weak)) get_calendar_day_period_symbol(StringView, StringView, CalendarPatternStyle, DayPeriod) { return OptionalNone {}; } 245ErrorOr<Optional<StringView>> __attribute__((weak)) get_calendar_day_period_symbol_for_hour(StringView, StringView, CalendarPatternStyle, u8) { return OptionalNone {}; } 246 247Optional<StringView> __attribute__((weak)) get_time_zone_name(StringView, StringView, CalendarPatternStyle, TimeZone::InDST) { return {}; } 248Optional<TimeZoneFormat> __attribute__((weak)) get_time_zone_format(StringView) { return {}; } 249 250static ErrorOr<Optional<String>> format_time_zone_offset(StringView locale, CalendarPatternStyle style, i64 offset_seconds) 251{ 252 auto formats = get_time_zone_format(locale); 253 if (!formats.has_value()) 254 return OptionalNone {}; 255 256 auto number_system = TRY(get_preferred_keyword_value_for_locale(locale, "nu"sv)); 257 if (!number_system.has_value()) 258 return OptionalNone {}; 259 260 if (offset_seconds == 0) 261 return String::from_utf8(formats->gmt_zero_format); 262 263 auto sign = offset_seconds > 0 ? formats->symbol_ahead_sign : formats->symbol_behind_sign; 264 auto separator = offset_seconds > 0 ? formats->symbol_ahead_separator : formats->symbol_behind_separator; 265 offset_seconds = llabs(offset_seconds); 266 267 auto offset_hours = offset_seconds / 3'600; 268 offset_seconds %= 3'600; 269 270 auto offset_minutes = offset_seconds / 60; 271 offset_seconds %= 60; 272 273 StringBuilder builder; 274 TRY(builder.try_append(sign)); 275 276 switch (style) { 277 // The long format always uses 2-digit hours field and minutes field, with optional 2-digit seconds field. 278 case CalendarPatternStyle::LongOffset: 279 TRY(builder.try_appendff("{:02}{}{:02}", offset_hours, separator, offset_minutes)); 280 if (offset_seconds > 0) 281 TRY(builder.try_appendff("{}{:02}", separator, offset_seconds)); 282 break; 283 284 // The short format is intended for the shortest representation and uses hour fields without leading zero, with optional 2-digit minutes and seconds fields. 285 case CalendarPatternStyle::ShortOffset: 286 TRY(builder.try_appendff("{}", offset_hours)); 287 if (offset_minutes > 0) { 288 TRY(builder.try_appendff("{}{:02}", separator, offset_minutes)); 289 if (offset_seconds > 0) 290 TRY(builder.try_appendff("{}{:02}", separator, offset_seconds)); 291 } 292 break; 293 294 default: 295 VERIFY_NOT_REACHED(); 296 } 297 298 // The digits used for hours, minutes and seconds fields in this format are the locale's default decimal digits. 299 auto result = TRY(replace_digits_for_number_system(*number_system, TRY(builder.to_string()))); 300 return TRY(String::from_utf8(formats->gmt_format)).replace("{0}"sv, result, ReplaceMode::FirstOnly); 301} 302 303// https://unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Format_Terminology 304ErrorOr<String> format_time_zone(StringView locale, StringView time_zone, CalendarPatternStyle style, AK::Time time) 305{ 306 auto offset = TimeZone::get_time_zone_offset(time_zone, time); 307 if (!offset.has_value()) 308 return String::from_utf8(time_zone); 309 310 switch (style) { 311 case CalendarPatternStyle::Short: 312 case CalendarPatternStyle::Long: 313 case CalendarPatternStyle::ShortGeneric: 314 case CalendarPatternStyle::LongGeneric: 315 if (auto name = get_time_zone_name(locale, time_zone, style, offset->in_dst); name.has_value()) 316 return String::from_utf8(*name); 317 break; 318 319 case CalendarPatternStyle::ShortOffset: 320 case CalendarPatternStyle::LongOffset: 321 if (auto formatted_offset = TRY(format_time_zone_offset(locale, style, offset->seconds)); formatted_offset.has_value()) 322 return formatted_offset.release_value(); 323 return String::from_utf8(time_zone); 324 325 default: 326 VERIFY_NOT_REACHED(); 327 } 328 329 // If more styles are added, consult the following table to ensure always falling back to GMT offset is still correct: 330 // https://unicode.org/reports/tr35/tr35-dates.html#dfst-zone 331 switch (style) { 332 case CalendarPatternStyle::Short: 333 case CalendarPatternStyle::ShortGeneric: 334 return format_time_zone(locale, time_zone, CalendarPatternStyle::ShortOffset, time); 335 336 case CalendarPatternStyle::Long: 337 case CalendarPatternStyle::LongGeneric: 338 return format_time_zone(locale, time_zone, CalendarPatternStyle::LongOffset, time); 339 340 default: 341 VERIFY_NOT_REACHED(); 342 } 343} 344 345}