Serenity Operating System
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}