/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 | | } |