Coverage Report

Created: 2026-11-13 08:07

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibJS/Runtime/Intl/Locale.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2021-2022, Tim Flynn <trflynn89@serenityos.org>
3
 *
4
 * SPDX-License-Identifier: BSD-2-Clause
5
 */
6
7
#include <AK/QuickSort.h>
8
#include <LibJS/Runtime/Array.h>
9
#include <LibJS/Runtime/GlobalObject.h>
10
#include <LibJS/Runtime/Intl/Locale.h>
11
#include <LibLocale/DateTimeFormat.h>
12
#include <LibLocale/Locale.h>
13
#include <LibTimeZone/TimeZone.h>
14
15
namespace JS::Intl {
16
17
JS_DEFINE_ALLOCATOR(Locale);
18
19
NonnullGCPtr<Locale> Locale::create(Realm& realm, ::Locale::LocaleID locale_id)
20
0
{
21
0
    auto locale = realm.heap().allocate<Locale>(realm, realm.intrinsics().intl_locale_prototype());
22
0
    locale->set_locale(locale_id.to_string());
23
24
0
    for (auto& extension : locale_id.extensions) {
25
0
        if (!extension.has<::Locale::LocaleExtension>())
26
0
            continue;
27
28
0
        for (auto& keyword : extension.get<::Locale::LocaleExtension>().keywords) {
29
0
            if (keyword.key == "ca"sv)
30
0
                locale->set_calendar(move(keyword.value));
31
0
            else if (keyword.key == "co"sv)
32
0
                locale->set_collation(move(keyword.value));
33
0
            else if (keyword.key == "hc"sv)
34
0
                locale->set_hour_cycle(move(keyword.value));
35
0
            else if (keyword.key == "kf"sv)
36
0
                locale->set_case_first(move(keyword.value));
37
0
            else if (keyword.key == "kn"sv)
38
0
                locale->set_numeric(keyword.value.is_empty());
39
0
            else if (keyword.key == "nu"sv)
40
0
                locale->set_numbering_system(move(keyword.value));
41
0
        }
42
43
0
        break;
44
0
    }
45
46
0
    return locale;
47
0
}
48
49
// 14 Locale Objects, https://tc39.es/ecma402/#locale-objects
50
Locale::Locale(Object& prototype)
51
0
    : Object(ConstructWithPrototypeTag::Tag, prototype)
52
0
{
53
0
}
54
55
// 1.1.1 CreateArrayFromListOrRestricted ( list , restricted )
56
static NonnullGCPtr<Array> create_array_from_list_or_restricted(VM& vm, Vector<StringView> list, Optional<String> restricted)
57
0
{
58
0
    auto& realm = *vm.current_realm();
59
60
    // 1. If restricted is not undefined, then
61
0
    if (restricted.has_value()) {
62
        // a. Set list to « restricted ».
63
0
        list = { *restricted };
64
0
    }
65
66
    // 2. Return ! CreateArrayFromList( list ).
67
0
    return Array::create_from<StringView>(realm, list, [&vm](auto value) {
68
0
        return PrimitiveString::create(vm, MUST(String::from_utf8(value)));
69
0
    });
70
0
}
71
72
// 1.1.2 CalendarsOfLocale ( loc ), https://tc39.es/proposal-intl-locale-info/#sec-calendars-of-locale
73
NonnullGCPtr<Array> calendars_of_locale(VM& vm, Locale const& locale_object)
74
0
{
75
    // 1. Let restricted be loc.[[Calendar]].
76
0
    Optional<String> restricted = locale_object.has_calendar() ? locale_object.calendar() : Optional<String> {};
77
78
    // 2. Let locale be loc.[[Locale]].
79
0
    auto const& locale = locale_object.locale();
80
81
    // 3. Assert: locale matches the unicode_locale_id production.
82
0
    VERIFY(::Locale::parse_unicode_locale_id(locale).has_value());
83
84
    // 4. Let list be a List of 1 or more unique canonical calendar identifiers, which must be lower case String values conforming to the type sequence from UTS 35 Unicode Locale Identifier, section 3.2, sorted in descending preference of those in common use for date and time formatting in locale.
85
0
    auto list = ::Locale::get_keywords_for_locale(locale, "ca"sv);
86
87
    // 5. Return ! CreateArrayFromListOrRestricted( list, restricted ).
88
0
    return create_array_from_list_or_restricted(vm, move(list), move(restricted));
89
0
}
90
91
// 1.1.3 CollationsOfLocale ( loc ), https://tc39.es/proposal-intl-locale-info/#sec-collations-of-locale
92
NonnullGCPtr<Array> collations_of_locale(VM& vm, Locale const& locale_object)
93
0
{
94
    // 1. Let restricted be loc.[[Collation]].
95
0
    Optional<String> restricted = locale_object.has_collation() ? locale_object.collation() : Optional<String> {};
96
97
    // 2. Let locale be loc.[[Locale]].
98
0
    auto const& locale = locale_object.locale();
99
100
    // 3. Assert: locale matches the unicode_locale_id production.
101
0
    VERIFY(::Locale::parse_unicode_locale_id(locale).has_value());
102
103
    // 4. Let list be a List of 1 or more unique canonical collation identifiers, which must be lower case String values conforming to the type sequence from UTS 35 Unicode Locale Identifier, section 3.2, ordered as if an Array of the same values had been sorted, using %Array.prototype.sort% using undefined as comparefn, of those in common use for string comparison in locale. The values "standard" and "search" must be excluded from list.
104
0
    auto list = ::Locale::get_keywords_for_locale(locale, "co"sv);
105
106
    // 5. Return ! CreateArrayFromListOrRestricted( list, restricted ).
107
0
    return create_array_from_list_or_restricted(vm, move(list), move(restricted));
108
0
}
109
110
// 1.1.4 HourCyclesOfLocale ( loc ), https://tc39.es/proposal-intl-locale-info/#sec-hour-cycles-of-locale
111
NonnullGCPtr<Array> hour_cycles_of_locale(VM& vm, Locale const& locale_object)
112
0
{
113
    // 1. Let restricted be loc.[[HourCycle]].
114
0
    Optional<String> restricted = locale_object.has_hour_cycle() ? locale_object.hour_cycle() : Optional<String> {};
115
116
    // 2. Let locale be loc.[[Locale]].
117
0
    auto const& locale = locale_object.locale();
118
119
    // 3. Assert: locale matches the unicode_locale_id production.
120
0
    VERIFY(::Locale::parse_unicode_locale_id(locale).has_value());
121
122
    // 4. Let list be a List of 1 or more unique hour cycle identifiers, which must be lower case String values indicating either the 12-hour format ("h11", "h12") or the 24-hour format ("h23", "h24"), sorted in descending preference of those in common use for date and time formatting in locale.
123
0
    auto list = ::Locale::get_keywords_for_locale(locale, "hc"sv);
124
125
    // 5. Return ! CreateArrayFromListOrRestricted( list, restricted ).
126
0
    return create_array_from_list_or_restricted(vm, move(list), move(restricted));
127
0
}
128
129
// 1.1.5 NumberingSystemsOfLocale ( loc ), https://tc39.es/proposal-intl-locale-info/#sec-numbering-systems-of-locale
130
NonnullGCPtr<Array> numbering_systems_of_locale(VM& vm, Locale const& locale_object)
131
0
{
132
    // 1. Let restricted be loc.[[NumberingSystem]].
133
0
    Optional<String> restricted = locale_object.has_numbering_system() ? locale_object.numbering_system() : Optional<String> {};
134
135
    // 2. Let locale be loc.[[Locale]].
136
0
    auto const& locale = locale_object.locale();
137
138
    // 3. Assert: locale matches the unicode_locale_id production.
139
0
    VERIFY(::Locale::parse_unicode_locale_id(locale).has_value());
140
141
    // 4. Let list be a List of 1 or more unique canonical numbering system identifiers, which must be lower case String values conforming to the type sequence from UTS 35 Unicode Locale Identifier, section 3.2, sorted in descending preference of those in common use for formatting numeric values in locale.
142
0
    auto list = ::Locale::get_keywords_for_locale(locale, "nu"sv);
143
144
    // 5. Return ! CreateArrayFromListOrRestricted( list, restricted ).
145
0
    return create_array_from_list_or_restricted(vm, move(list), move(restricted));
146
0
}
147
148
// 1.1.6 TimeZonesOfLocale ( loc ), https://tc39.es/proposal-intl-locale-info/#sec-time-zones-of-locale
149
// NOTE: Our implementation takes a region rather than a Locale object to avoid needlessly parsing the locale twice.
150
NonnullGCPtr<Array> time_zones_of_locale(VM& vm, StringView region)
151
0
{
152
0
    auto& realm = *vm.current_realm();
153
154
    // 1. Let locale be loc.[[Locale]].
155
    // 2. Assert: locale matches the unicode_locale_id production.
156
    // 3. Let region be the substring of locale corresponding to the unicode_region_subtag production of the unicode_language_id.
157
158
    // 4. Let list be a List of unique canonical time zone identifiers, which must be String values indicating a canonical Zone name of the IANA Time Zone Database, ordered as if an Array of the same values had been sorted using %Array.prototype.sort% using undefined as comparefn, of those in common use in region. If no time zones are commonly used in region, let list be a new empty List.
159
0
    auto list = TimeZone::time_zones_in_region(region);
160
0
    quick_sort(list);
161
162
    // 5. Return ! CreateArrayFromList( list ).
163
0
    return Array::create_from<StringView>(realm, list, [&vm](auto value) {
164
0
        return PrimitiveString::create(vm, String::from_utf8(value).release_value());
165
0
    });
166
0
}
167
168
// 1.1.7 CharacterDirectionOfLocale ( loc ), https://tc39.es/proposal-intl-locale-info/#sec-character-direction-of-locale
169
StringView character_direction_of_locale(Locale const& locale_object)
170
0
{
171
    // 1. Let locale be loc.[[Locale]].
172
0
    auto const& locale = locale_object.locale();
173
174
    // 2. Assert: locale matches the unicode_locale_id production.
175
0
    VERIFY(::Locale::parse_unicode_locale_id(locale).has_value());
176
177
    // 3. If the default general ordering of characters (characterOrder) within a line in locale is right-to-left, return "rtl".
178
    // NOTE: LibUnicode handles both LTR and RTL character orders in this call, not just RTL. We then fallback to LTR
179
    //       below if LibUnicode doesn't conclusively know the character order for this locale.
180
0
    if (auto character_order = ::Locale::character_order_for_locale(locale); character_order.has_value())
181
0
        return ::Locale::character_order_to_string(*character_order);
182
183
    // 4. Return "ltr".
184
0
    return "ltr"sv;
185
0
}
186
187
// 1.1.8 WeekdayToNumber ( fw ), https://tc39.es/proposal-intl-locale-info/#sec-weekday-to-number
188
// FIXME: Spec issue: The spec definitions of WeekdayToNumber and WeekdayToString are backwards.
189
//        https://github.com/tc39/proposal-intl-locale-info/issues/78
190
Optional<u8> weekday_to_number(StringView weekday)
191
0
{
192
0
    struct WeekdayToNumber {
193
0
        StringView type;
194
0
        u8 number { 0 };
195
0
    };
196
197
    // Table 2: First Day Type and Number, https://tc39.es/proposal-intl-locale-info/#table-locale-first-day-type-number
198
0
    static constexpr auto weekday_to_number_table = AK::Array {
199
0
        WeekdayToNumber { "mon"sv, 1 },
200
0
        WeekdayToNumber { "tue"sv, 2 },
201
0
        WeekdayToNumber { "wed"sv, 3 },
202
0
        WeekdayToNumber { "thu"sv, 4 },
203
0
        WeekdayToNumber { "fri"sv, 5 },
204
0
        WeekdayToNumber { "sat"sv, 6 },
205
0
        WeekdayToNumber { "sun"sv, 7 },
206
0
    };
207
208
    // 1. For each row of Table 2, except the header row, in table order, do
209
0
    for (auto const& row : weekday_to_number_table) {
210
        // a. Let t be the name given in the Type column of the row.
211
0
        auto type = row.type;
212
213
        // b. Let n be the name given in the Number column of the row.
214
0
        auto number = row.number;
215
216
        // c. If fw is equal to t, return n.
217
0
        if (weekday == type)
218
0
            return number;
219
0
    }
220
221
    // 2. Assert: Should not reach here.
222
    // FIXME: Spec issue: This is currently reachable if an invalid value is provided as a locale extension,
223
    //        for example "en-u-fw-100". We return "undefined" for now to avoid crashing.
224
    //        https://github.com/tc39/proposal-intl-locale-info/issues/78
225
0
    return {};
226
0
}
227
228
// 1.1.9 WeekdayToString ( fw ), https://tc39.es/proposal-intl-locale-info/#sec-weekday-to-string
229
// FIXME: Spec issue: The spec definitions of WeekdayToNumber and WeekdayToString are backwards.
230
//        https://github.com/tc39/proposal-intl-locale-info/issues/78
231
StringView weekday_to_string(StringView weekday)
232
0
{
233
0
    struct WeekdayToString {
234
0
        StringView value;
235
0
        StringView type;
236
0
    };
237
238
    // Table 1: First Day Value and Type, https://tc39.es/proposal-intl-locale-info/#table-locale-first-day-option-type
239
0
    static constexpr auto weekday_to_string_table = AK::Array {
240
0
        WeekdayToString { "mon"sv, "mon"sv },
241
0
        WeekdayToString { "tue"sv, "tue"sv },
242
0
        WeekdayToString { "wed"sv, "wed"sv },
243
0
        WeekdayToString { "thu"sv, "thu"sv },
244
0
        WeekdayToString { "fri"sv, "fri"sv },
245
0
        WeekdayToString { "sat"sv, "sat"sv },
246
0
        WeekdayToString { "sun"sv, "sun"sv },
247
0
        WeekdayToString { "0"sv, "sun"sv },
248
0
        WeekdayToString { "1"sv, "mon"sv },
249
0
        WeekdayToString { "2"sv, "tue"sv },
250
0
        WeekdayToString { "3"sv, "wed"sv },
251
0
        WeekdayToString { "4"sv, "thu"sv },
252
0
        WeekdayToString { "5"sv, "fri"sv },
253
0
        WeekdayToString { "6"sv, "sat"sv },
254
0
        WeekdayToString { "7"sv, "sun"sv },
255
0
    };
256
257
    // 1. For each row of Table 1, except the header row, in table order, do
258
0
    for (auto const& row : weekday_to_string_table) {
259
        // a. Let v be the name given in the Value column of the row.
260
0
        auto value = row.value;
261
262
        // b. Let t be the name given in the Type column of the row.
263
0
        auto type = row.type;
264
265
        // c. If fw is equal to v, return t.
266
0
        if (weekday == value)
267
0
            return type;
268
0
    }
269
270
    // 2. Assert: Should not reach here.
271
0
    VERIFY_NOT_REACHED();
272
0
}
273
274
static u8 weekday_to_integer(Optional<::Locale::Weekday> weekday, ::Locale::Weekday falllback)
275
0
{
276
    // NOTE: This fallback will be used if LibUnicode data generation is disabled. Its value should
277
    //       be that of the default region ("001") in the CLDR.
278
0
    switch (weekday.value_or(falllback)) {
279
0
    case ::Locale::Weekday::Monday:
280
0
        return 1;
281
0
    case ::Locale::Weekday::Tuesday:
282
0
        return 2;
283
0
    case ::Locale::Weekday::Wednesday:
284
0
        return 3;
285
0
    case ::Locale::Weekday::Thursday:
286
0
        return 4;
287
0
    case ::Locale::Weekday::Friday:
288
0
        return 5;
289
0
    case ::Locale::Weekday::Saturday:
290
0
        return 6;
291
0
    case ::Locale::Weekday::Sunday:
292
0
        return 7;
293
0
    }
294
295
0
    VERIFY_NOT_REACHED();
296
0
}
297
298
static Vector<u8> weekend_of_locale(StringView locale)
299
0
{
300
0
    auto weekend_start = weekday_to_integer(::Locale::get_locale_weekend_start(locale), ::Locale::Weekday::Saturday);
301
0
    auto weekend_end = weekday_to_integer(::Locale::get_locale_weekend_end(locale), ::Locale::Weekday::Sunday);
302
303
    // There currently aren't any regions in the CLDR which wrap around from Sunday (7) to Monday (1).
304
    // If this changes, this logic will need to be updated to handle that.
305
0
    VERIFY(weekend_start <= weekend_end);
306
307
0
    Vector<u8> weekend;
308
0
    weekend.ensure_capacity(weekend_end - weekend_start + 1);
309
310
0
    for (auto day = weekend_start; day <= weekend_end; ++day)
311
0
        weekend.unchecked_append(day);
312
313
0
    return weekend;
314
0
}
315
316
// 1.1.10 WeekInfoOfLocale ( loc ), https://tc39.es/proposal-intl-locale-info/#sec-week-info-of-locale
317
WeekInfo week_info_of_locale(Locale const& locale_object)
318
0
{
319
    // 1. Let locale be loc.[[Locale]].
320
0
    auto const& locale = locale_object.locale();
321
322
    // 2. Assert: locale matches the unicode_locale_id production.
323
0
    VERIFY(::Locale::parse_unicode_locale_id(locale).has_value());
324
325
    // 3. Let r be a record whose fields are defined by Table 3, with values based on locale.
326
0
    WeekInfo week_info {};
327
0
    week_info.minimal_days = ::Locale::get_locale_minimum_days(locale).value_or(1);
328
0
    week_info.first_day = weekday_to_integer(::Locale::get_locale_first_day(locale), ::Locale::Weekday::Monday);
329
0
    week_info.weekend = weekend_of_locale(locale);
330
331
    // 4. Let fw be loc.[[FirstDayOfWeek]].
332
    // 5. If fw is not undefined, then
333
0
    if (locale_object.has_first_day_of_week()) {
334
        // a. Set r.[[FirstDay]] to fw.
335
0
        week_info.first_day = locale_object.first_day_of_week();
336
0
    }
337
338
    // 6. Return r.
339
0
    return week_info;
340
0
}
341
342
}