Coverage Report

Created: 2026-05-16 07:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibWeb/HTML/Dates.cpp
Line
Count
Source
1
/*
2
 * Copyright (c) 2018-2023, Andreas Kling <kling@serenityos.org>
3
 * Copyright (c) 2022, Adam Hodgen <ant1441@gmail.com>
4
 * Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
5
 * Copyright (c) 2023, Shannon Booth <shannon@serenityos.org>
6
 * Copyright (c) 2023, stelar7 <dudedbz@gmail.com>
7
 *
8
 * SPDX-License-Identifier: BSD-2-Clause
9
 */
10
11
#include <AK/Time.h>
12
#include <LibWeb/HTML/Dates.h>
13
14
namespace Web::HTML {
15
16
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#week-number-of-the-last-day
17
u32 week_number_of_the_last_day(u64 year)
18
0
{
19
    // https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#weeks
20
    // NOTE: A year is considered to have 53 weeks if either of the following conditions are satisfied:
21
    // - January 1 of that year is a Thursday.
22
    // - January 1 of that year is a Wednesday and the year is divisible by 400, or divisible by 4, but not 100.
23
24
    // Note: Gauss's algorithm for determining the day of the week with D = 1, and M = 0
25
    // https://en.wikipedia.org/wiki/Determination_of_the_day_of_the_week#Gauss's_algorithm
26
0
    u8 day_of_week = (1 + 5 * ((year - 1) % 4) + 4 * ((year - 1) % 100) + 6 * ((year - 1) % 400)) % 7;
27
28
0
    if (day_of_week == 4 || (day_of_week == 3 && (year % 400 == 0 || (year % 4 == 0 && year % 100 != 0))))
29
0
        return 53;
30
31
0
    return 52;
32
0
}
33
34
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-week-string
35
bool is_valid_week_string(StringView value)
36
0
{
37
    // A string is a valid week string representing a week-year year and week week if it consists of the following components in the given order:
38
39
    // 1. Four or more ASCII digits, representing year, where year > 0
40
    // 2. A U+002D HYPHEN-MINUS character (-)
41
    // 3. A U+0057 LATIN CAPITAL LETTER W character (W)
42
    // 4. Two ASCII digits, representing the week week, in the range 1 ≤ week ≤ maxweek, where maxweek is the week number of the last day of week-year year
43
0
    auto parts = value.split_view('-', SplitBehavior::KeepEmpty);
44
0
    if (parts.size() != 2)
45
0
        return false;
46
0
    if (parts[0].length() < 4)
47
0
        return false;
48
0
    for (auto digit : parts[0])
49
0
        if (!is_ascii_digit(digit))
50
0
            return false;
51
0
    if (parts[1].length() != 3)
52
0
        return false;
53
54
0
    if (!parts[1].starts_with('W'))
55
0
        return false;
56
0
    if (!is_ascii_digit(parts[1][1]))
57
0
        return false;
58
0
    if (!is_ascii_digit(parts[1][2]))
59
0
        return false;
60
61
0
    u64 year = 0;
62
0
    for (auto d : parts[0]) {
63
0
        year *= 10;
64
0
        year += parse_ascii_digit(d);
65
0
    }
66
0
    auto week = (parse_ascii_digit(parts[1][1]) * 10) + parse_ascii_digit(parts[1][2]);
67
68
0
    return week >= 1 && week <= week_number_of_the_last_day(year);
69
0
}
70
71
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-month-string
72
bool is_valid_month_string(StringView value)
73
0
{
74
    // A string is a valid month string representing a year year and month month if it consists of the following components in the given order:
75
76
    // 1. Four or more ASCII digits, representing year, where year > 0
77
    // 2. A U+002D HYPHEN-MINUS character (-)
78
    // 3. Two ASCII digits, representing the month month, in the range 1 ≤ month ≤ 12
79
80
0
    auto parts = value.split_view('-', SplitBehavior::KeepEmpty);
81
0
    if (parts.size() != 2)
82
0
        return false;
83
84
0
    if (parts[0].length() < 4)
85
0
        return false;
86
0
    for (auto digit : parts[0])
87
0
        if (!is_ascii_digit(digit))
88
0
            return false;
89
90
0
    if (parts[1].length() != 2)
91
0
        return false;
92
93
0
    if (!is_ascii_digit(parts[1][0]))
94
0
        return false;
95
0
    if (!is_ascii_digit(parts[1][1]))
96
0
        return false;
97
98
0
    auto month = (parse_ascii_digit(parts[1][0]) * 10) + parse_ascii_digit(parts[1][1]);
99
0
    return month >= 1 && month <= 12;
100
0
}
101
102
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-date-string
103
bool is_valid_date_string(StringView value)
104
0
{
105
    // A string is a valid date string representing a year year, month month, and day day if it consists of the following components in the given order:
106
107
    // 1. A valid month string, representing year and month
108
    // 2. A U+002D HYPHEN-MINUS character (-)
109
    // 3. Two ASCII digits, representing day, in the range 1 ≤ day ≤ maxday where maxday is the number of days in the month month and year year
110
0
    auto parts = value.split_view('-', SplitBehavior::KeepEmpty);
111
0
    if (parts.size() != 3)
112
0
        return false;
113
114
0
    if (!is_valid_month_string(ByteString::formatted("{}-{}", parts[0], parts[1])))
115
0
        return false;
116
117
0
    if (parts[2].length() != 2)
118
0
        return false;
119
120
0
    i64 year = 0;
121
0
    for (auto d : parts[0]) {
122
0
        year *= 10;
123
0
        year += parse_ascii_digit(d);
124
0
    }
125
0
    auto month = (parse_ascii_digit(parts[1][0]) * 10) + parse_ascii_digit(parts[1][1]);
126
0
    i64 day = (parse_ascii_digit(parts[2][0]) * 10) + parse_ascii_digit(parts[2][1]);
127
128
0
    return day >= 1 && day <= AK::days_in_month(year, month);
129
0
}
130
131
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-date-string
132
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Date>> parse_date_string(JS::Realm& realm, StringView value)
133
0
{
134
    // FIXME: Implement spec compliant date string parsing
135
0
    auto parts = value.split_view('-', SplitBehavior::KeepEmpty);
136
0
    if (parts.size() >= 3) {
137
0
        if (auto year = parts.at(0).to_number<u32>(); year.has_value()) {
138
0
            if (auto month = parts.at(1).to_number<u32>(); month.has_value()) {
139
0
                if (auto day_of_month = parts.at(2).to_number<u32>(); day_of_month.has_value())
140
0
                    return JS::Date::create(realm, JS::make_date(JS::make_day(*year, *month - 1, *day_of_month), 0));
141
0
            }
142
0
        }
143
0
    }
144
0
    return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Can't parse date string"sv };
145
0
}
146
147
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-local-date-and-time-string
148
bool is_valid_local_date_and_time_string(StringView value)
149
0
{
150
0
    auto parts_split_by_T = value.split_view('T', SplitBehavior::KeepEmpty);
151
0
    if (parts_split_by_T.size() == 2)
152
0
        return is_valid_date_string(parts_split_by_T[0]) && is_valid_time_string(parts_split_by_T[1]);
153
0
    auto parts_split_by_space = value.split_view(' ', SplitBehavior::KeepEmpty);
154
0
    if (parts_split_by_space.size() == 2)
155
0
        return is_valid_date_string(parts_split_by_space[0]) && is_valid_time_string(parts_split_by_space[1]);
156
157
0
    return false;
158
0
}
159
160
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-normalised-local-date-and-time-string
161
String normalize_local_date_and_time_string(String const& value)
162
0
{
163
0
    if (auto spaces = value.count(" "sv); spaces > 0) {
164
0
        VERIFY(spaces == 1);
165
0
        return MUST(value.replace(" "sv, "T"sv, ReplaceMode::FirstOnly));
166
0
    }
167
168
0
    VERIFY(value.count("T"sv) == 1);
169
0
    return value;
170
0
}
171
172
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#valid-time-string
173
bool is_valid_time_string(StringView value)
174
0
{
175
    // A string is a valid time string representing an hour hour, a minute minute, and a second second if it consists of the following components in the given order:
176
177
    // 1. Two ASCII digits, representing hour, in the range 0 ≤ hour ≤ 23
178
    // 2. A U+003A COLON character (:)
179
    // 3. Two ASCII digits, representing minute, in the range 0 ≤ minute ≤ 59
180
    // 4. If second is nonzero, or optionally if second is zero:
181
    // 1. A U+003A COLON character (:)
182
    // 2. Two ASCII digits, representing the integer part of second, in the range 0 ≤ s ≤ 59
183
    // 3. If second is not an integer, or optionally if second is an integer:
184
    // 1. A U+002E FULL STOP character (.)
185
    // 2. One, two, or three ASCII digits, representing the fractional part of second
186
0
    auto parts = value.split_view(':', SplitBehavior::KeepEmpty);
187
0
    if (parts.size() != 2 && parts.size() != 3)
188
0
        return false;
189
0
    if (parts[0].length() != 2)
190
0
        return false;
191
0
    if (!(is_ascii_digit(parts[0][0]) && is_ascii_digit(parts[0][1])))
192
0
        return false;
193
0
    auto hour = (parse_ascii_digit(parts[0][0]) * 10) + parse_ascii_digit(parts[0][1]);
194
0
    if (hour > 23)
195
0
        return false;
196
0
    if (parts[1].length() != 2)
197
0
        return false;
198
0
    if (!(is_ascii_digit(parts[1][0]) && is_ascii_digit(parts[1][1])))
199
0
        return false;
200
0
    auto minute = (parse_ascii_digit(parts[1][0]) * 10) + parse_ascii_digit(parts[1][1]);
201
0
    if (minute > 59)
202
0
        return false;
203
0
    if (parts.size() == 2)
204
0
        return true;
205
206
0
    if (parts[2].length() < 2)
207
0
        return false;
208
0
    if (!(is_ascii_digit(parts[2][0]) && is_ascii_digit(parts[2][1])))
209
0
        return false;
210
0
    auto second = (parse_ascii_digit(parts[2][0]) * 10) + parse_ascii_digit(parts[2][1]);
211
0
    if (second > 59)
212
0
        return false;
213
0
    if (parts[2].length() == 2)
214
0
        return true;
215
0
    auto second_parts = parts[2].split_view('.', SplitBehavior::KeepEmpty);
216
0
    if (second_parts.size() != 2)
217
0
        return false;
218
0
    if (second_parts[1].length() < 1 || second_parts[1].length() > 3)
219
0
        return false;
220
0
    for (auto digit : second_parts[1])
221
0
        if (!is_ascii_digit(digit))
222
0
            return false;
223
224
0
    return true;
225
0
}
226
227
// https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#parse-a-time-string
228
WebIDL::ExceptionOr<JS::NonnullGCPtr<JS::Date>> parse_time_string(JS::Realm& realm, StringView value)
229
0
{
230
    // FIXME: Implement spec compliant time string parsing
231
0
    auto parts = value.split_view(':', SplitBehavior::KeepEmpty);
232
0
    if (parts.size() >= 2) {
233
0
        if (auto hours = parts.at(0).to_number<u32>(); hours.has_value()) {
234
0
            if (auto minutes = parts.at(1).to_number<u32>(); minutes.has_value()) {
235
0
                if (parts.size() >= 3) {
236
0
                    if (auto seconds = parts.at(2).to_number<u32>(); seconds.has_value())
237
0
                        return JS::Date::create(realm, JS::make_time(*hours, *minutes, *seconds, 0));
238
0
                }
239
0
                return JS::Date::create(realm, JS::make_date(0, JS::make_time(*hours, *minutes, 0, 0)));
240
0
            }
241
0
        }
242
0
    }
243
0
    return WebIDL::SimpleException { WebIDL::SimpleExceptionType::TypeError, "Can't parse time string"sv };
244
0
}
245
246
}