Coverage Report

Created: 2025-12-18 07:52

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