Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/CharacterTypes.h>
8#include <AK/DateConstants.h>
9#include <AK/StringBuilder.h>
10#include <AK/Time.h>
11#include <LibCore/DateTime.h>
12#include <errno.h>
13#include <time.h>
14
15namespace Core {
16
17DateTime DateTime::now()
18{
19 return from_timestamp(time(nullptr));
20}
21
22DateTime DateTime::create(int year, int month, int day, int hour, int minute, int second)
23{
24 DateTime dt;
25 dt.set_time(year, month, day, hour, minute, second);
26 return dt;
27}
28
29DateTime DateTime::from_timestamp(time_t timestamp)
30{
31 struct tm tm;
32 localtime_r(×tamp, &tm);
33 DateTime dt;
34 dt.m_year = tm.tm_year + 1900;
35 dt.m_month = tm.tm_mon + 1;
36 dt.m_day = tm.tm_mday;
37 dt.m_hour = tm.tm_hour;
38 dt.m_minute = tm.tm_min;
39 dt.m_second = tm.tm_sec;
40 dt.m_timestamp = timestamp;
41 return dt;
42}
43
44unsigned DateTime::weekday() const
45{
46 return ::day_of_week(m_year, m_month, m_day);
47}
48
49unsigned DateTime::days_in_month() const
50{
51 return ::days_in_month(m_year, m_month);
52}
53
54unsigned DateTime::day_of_year() const
55{
56 return ::day_of_year(m_year, m_month, m_day);
57}
58
59bool DateTime::is_leap_year() const
60{
61 return ::is_leap_year(m_year);
62}
63
64void DateTime::set_time(int year, int month, int day, int hour, int minute, int second)
65{
66 struct tm tm = {};
67 tm.tm_sec = second;
68 tm.tm_min = minute;
69 tm.tm_hour = hour;
70 tm.tm_mday = day;
71 tm.tm_mon = month - 1;
72 tm.tm_year = year - 1900;
73 tm.tm_isdst = -1;
74 // mktime() doesn't read tm.tm_wday and tm.tm_yday, no need to fill them in.
75
76 m_timestamp = mktime(&tm);
77
78 // mktime() normalizes the components to the right ranges (Jan 32 -> Feb 1 etc), so read fields back out from tm.
79 m_year = tm.tm_year + 1900;
80 m_month = tm.tm_mon + 1;
81 m_day = tm.tm_mday;
82 m_hour = tm.tm_hour;
83 m_minute = tm.tm_min;
84 m_second = tm.tm_sec;
85}
86
87DeprecatedString DateTime::to_deprecated_string(StringView format) const
88{
89 struct tm tm;
90 localtime_r(&m_timestamp, &tm);
91 StringBuilder builder;
92 int const format_len = format.length();
93
94 auto format_time_zone_offset = [&](bool with_separator) {
95 struct tm gmt_tm;
96 gmtime_r(&m_timestamp, &gmt_tm);
97
98 gmt_tm.tm_isdst = -1;
99 auto gmt_timestamp = mktime(&gmt_tm);
100
101 auto offset_seconds = static_cast<time_t>(difftime(m_timestamp, gmt_timestamp));
102 StringView offset_sign;
103
104 if (offset_seconds >= 0) {
105 offset_sign = "+"sv;
106 } else {
107 offset_sign = "-"sv;
108 offset_seconds *= -1;
109 }
110
111 auto offset_hours = offset_seconds / 3600;
112 auto offset_minutes = (offset_seconds % 3600) / 60;
113 auto separator = with_separator ? ":"sv : ""sv;
114
115 builder.appendff("{}{:02}{}{:02}", offset_sign, offset_hours, separator, offset_minutes);
116 };
117
118 for (int i = 0; i < format_len; ++i) {
119 if (format[i] != '%') {
120 builder.append(format[i]);
121 } else {
122 if (++i == format_len)
123 return DeprecatedString();
124
125 switch (format[i]) {
126 case 'a':
127 builder.append(short_day_names[tm.tm_wday]);
128 break;
129 case 'A':
130 builder.append(long_day_names[tm.tm_wday]);
131 break;
132 case 'b':
133 builder.append(short_month_names[tm.tm_mon]);
134 break;
135 case 'B':
136 builder.append(long_month_names[tm.tm_mon]);
137 break;
138 case 'C':
139 builder.appendff("{:02}", (tm.tm_year + 1900) / 100);
140 break;
141 case 'd':
142 builder.appendff("{:02}", tm.tm_mday);
143 break;
144 case 'D':
145 builder.appendff("{:02}/{:02}/{:02}", tm.tm_mon + 1, tm.tm_mday, (tm.tm_year + 1900) % 100);
146 break;
147 case 'e':
148 builder.appendff("{:2}", tm.tm_mday);
149 break;
150 case 'h':
151 builder.append(short_month_names[tm.tm_mon]);
152 break;
153 case 'H':
154 builder.appendff("{:02}", tm.tm_hour);
155 break;
156 case 'I': {
157 int display_hour = tm.tm_hour % 12;
158 if (display_hour == 0)
159 display_hour = 12;
160 builder.appendff("{:02}", display_hour);
161 break;
162 }
163 case 'j':
164 builder.appendff("{:03}", tm.tm_yday + 1);
165 break;
166 case 'l': {
167 int display_hour = tm.tm_hour % 12;
168 if (display_hour == 0)
169 display_hour = 12;
170 builder.appendff("{:2}", display_hour);
171 break;
172 }
173 case 'm':
174 builder.appendff("{:02}", tm.tm_mon + 1);
175 break;
176 case 'M':
177 builder.appendff("{:02}", tm.tm_min);
178 break;
179 case 'n':
180 builder.append('\n');
181 break;
182 case 'p':
183 builder.append(tm.tm_hour < 12 ? "AM"sv : "PM"sv);
184 break;
185 case 'r': {
186 int display_hour = tm.tm_hour % 12;
187 if (display_hour == 0)
188 display_hour = 12;
189 builder.appendff("{:02}:{:02}:{:02} {}", display_hour, tm.tm_min, tm.tm_sec, tm.tm_hour < 12 ? "AM" : "PM");
190 break;
191 }
192 case 'R':
193 builder.appendff("{:02}:{:02}", tm.tm_hour, tm.tm_min);
194 break;
195 case 'S':
196 builder.appendff("{:02}", tm.tm_sec);
197 break;
198 case 't':
199 builder.append('\t');
200 break;
201 case 'T':
202 builder.appendff("{:02}:{:02}:{:02}", tm.tm_hour, tm.tm_min, tm.tm_sec);
203 break;
204 case 'u':
205 builder.appendff("{}", tm.tm_wday ? tm.tm_wday : 7);
206 break;
207 case 'U': {
208 int const wday_of_year_beginning = (tm.tm_wday + 6 * tm.tm_yday) % 7;
209 int const week_number = (tm.tm_yday + wday_of_year_beginning) / 7;
210 builder.appendff("{:02}", week_number);
211 break;
212 }
213 case 'V': {
214 int const wday_of_year_beginning = (tm.tm_wday + 6 + 6 * tm.tm_yday) % 7;
215 int week_number = (tm.tm_yday + wday_of_year_beginning) / 7 + 1;
216 if (wday_of_year_beginning > 3) {
217 if (tm.tm_yday >= 7 - wday_of_year_beginning)
218 --week_number;
219 else {
220 int const days_of_last_year = days_in_year(tm.tm_year + 1900 - 1);
221 int const wday_of_last_year_beginning = (wday_of_year_beginning + 6 * days_of_last_year) % 7;
222 week_number = (days_of_last_year + wday_of_last_year_beginning) / 7 + 1;
223 if (wday_of_last_year_beginning > 3)
224 --week_number;
225 }
226 }
227 builder.appendff("{:02}", week_number);
228 break;
229 }
230 case 'w':
231 builder.appendff("{}", tm.tm_wday);
232 break;
233 case 'W': {
234 int const wday_of_year_beginning = (tm.tm_wday + 6 + 6 * tm.tm_yday) % 7;
235 int const week_number = (tm.tm_yday + wday_of_year_beginning) / 7;
236 builder.appendff("{:02}", week_number);
237 break;
238 }
239 case 'y':
240 builder.appendff("{:02}", (tm.tm_year + 1900) % 100);
241 break;
242 case 'Y':
243 builder.appendff("{}", tm.tm_year + 1900);
244 break;
245 case 'z':
246 format_time_zone_offset(false);
247 break;
248 case ':':
249 if (++i == format_len) {
250 builder.append("%:"sv);
251 break;
252 }
253 if (format[i] != 'z') {
254 builder.append("%:"sv);
255 builder.append(format[i]);
256 break;
257 }
258 format_time_zone_offset(true);
259 break;
260 case 'Z': {
261 auto const* timezone_name = tzname[tm.tm_isdst == 0 ? 0 : 1];
262 builder.append({ timezone_name, strlen(timezone_name) });
263 break;
264 }
265 case '%':
266 builder.append('%');
267 break;
268 default:
269 builder.append('%');
270 builder.append(format[i]);
271 break;
272 }
273 }
274 }
275
276 return builder.to_deprecated_string();
277}
278
279Optional<DateTime> DateTime::parse(StringView format, DeprecatedString const& string)
280{
281 unsigned format_pos = 0;
282 unsigned string_pos = 0;
283 struct tm tm = {};
284
285 auto parsing_failed = false;
286 auto tm_represents_utc_time = false;
287
288 auto parse_number = [&] {
289 if (string_pos >= string.length()) {
290 parsing_failed = true;
291 return 0;
292 }
293
294 char* end_ptr = nullptr;
295 errno = 0;
296 int number = strtol(string.characters() + string_pos, &end_ptr, 10);
297
298 auto chars_parsed = end_ptr - (string.characters() + string_pos);
299 if (chars_parsed == 0 || errno != 0)
300 parsing_failed = true;
301 else
302 string_pos += chars_parsed;
303 return number;
304 };
305
306 auto consume = [&](char x) {
307 if (string_pos >= string.length()) {
308 parsing_failed = true;
309 return;
310 }
311 if (string[string_pos] != x)
312 parsing_failed = true;
313 else
314 string_pos++;
315 };
316
317 while (format_pos < format.length() && string_pos < string.length()) {
318 if (format[format_pos] != '%') {
319 if (format[format_pos] != string[string_pos]) {
320 return {};
321 }
322 format_pos++;
323 string_pos++;
324 continue;
325 }
326
327 format_pos++;
328 if (format_pos == format.length()) {
329 return {};
330 }
331 switch (format[format_pos]) {
332 case 'a': {
333 auto wday = 0;
334 for (auto name : short_day_names) {
335 if (string.substring_view(string_pos).starts_with(name, AK::CaseSensitivity::CaseInsensitive)) {
336 string_pos += name.length();
337 tm.tm_wday = wday;
338 break;
339 }
340 ++wday;
341 }
342 if (wday == 7)
343 return {};
344 break;
345 }
346 case 'A': {
347 auto wday = 0;
348 for (auto name : long_day_names) {
349 if (string.substring_view(string_pos).starts_with(name, AK::CaseSensitivity::CaseInsensitive)) {
350 string_pos += name.length();
351 tm.tm_wday = wday;
352 break;
353 }
354 ++wday;
355 }
356 if (wday == 7)
357 return {};
358 break;
359 }
360 case 'h':
361 case 'b': {
362 auto mon = 0;
363 for (auto name : short_month_names) {
364 if (string.substring_view(string_pos).starts_with(name, AK::CaseSensitivity::CaseInsensitive)) {
365 string_pos += name.length();
366 tm.tm_mon = mon;
367 break;
368 }
369 ++mon;
370 }
371 if (mon == 12)
372 return {};
373 break;
374 }
375 case 'B': {
376 auto mon = 0;
377 for (auto name : long_month_names) {
378 if (string.substring_view(string_pos).starts_with(name, AK::CaseSensitivity::CaseInsensitive)) {
379 string_pos += name.length();
380 tm.tm_mon = mon;
381 break;
382 }
383 ++mon;
384 }
385 if (mon == 12)
386 return {};
387 break;
388 }
389 case 'C': {
390 int num = parse_number();
391 tm.tm_year = (num - 19) * 100;
392 break;
393 }
394 case 'd': {
395 tm.tm_mday = parse_number();
396 break;
397 }
398 case 'D': {
399 int mon = parse_number();
400 consume('/');
401 int day = parse_number();
402 consume('/');
403 int year = parse_number();
404 tm.tm_mon = mon + 1;
405 tm.tm_mday = day;
406 tm.tm_year = (year + 1900) % 100;
407 break;
408 }
409 case 'e': {
410 tm.tm_mday = parse_number();
411 break;
412 }
413 case 'H': {
414 tm.tm_hour = parse_number();
415 break;
416 }
417 case 'I': {
418 int num = parse_number();
419 tm.tm_hour = num % 12;
420 break;
421 }
422 case 'j': {
423 // a little trickery here... we can get mktime() to figure out mon and mday using out of range values.
424 // yday is not used so setting it is pointless.
425 tm.tm_mday = parse_number();
426 tm.tm_mon = 0;
427 mktime(&tm);
428 break;
429 }
430 case 'm': {
431 int num = parse_number();
432 tm.tm_mon = num - 1;
433 break;
434 }
435 case 'M': {
436 tm.tm_min = parse_number();
437 break;
438 }
439 case 'n':
440 case 't':
441 while (is_ascii_blank(string[string_pos])) {
442 string_pos++;
443 }
444 break;
445 case 'p': {
446 auto ampm = string.substring_view(string_pos, 2);
447 if (ampm == "PM" && tm.tm_hour < 12) {
448 tm.tm_hour += 12;
449 }
450 string_pos += 2;
451 break;
452 }
453 case 'r': {
454 auto ampm = string.substring_view(string_pos, 2);
455 if (ampm == "PM" && tm.tm_hour < 12) {
456 tm.tm_hour += 12;
457 }
458 string_pos += 2;
459 break;
460 }
461 case 'R': {
462 tm.tm_hour = parse_number();
463 consume(':');
464 tm.tm_min = parse_number();
465 break;
466 }
467 case 'S':
468 tm.tm_sec = parse_number();
469 break;
470 case 'T':
471 tm.tm_hour = parse_number();
472 consume(':');
473 tm.tm_min = parse_number();
474 consume(':');
475 tm.tm_sec = parse_number();
476 break;
477 case 'w':
478 tm.tm_wday = parse_number();
479 break;
480 case 'y': {
481 int year = parse_number();
482 tm.tm_year = year <= 99 && year > 69 ? 1900 + year : 2000 + year;
483 break;
484 }
485 case 'Y': {
486 int year = parse_number();
487 tm.tm_year = year - 1900;
488 break;
489 }
490 case 'z': {
491 tm_represents_utc_time = true;
492 if (string[string_pos] == 'Z') {
493 // UTC time
494 string_pos++;
495 break;
496 }
497 int sign;
498
499 if (string[string_pos] == '+')
500 sign = -1;
501 else if (string[string_pos] == '-')
502 sign = +1;
503 else
504 return {};
505
506 string_pos++;
507
508 auto hours = parse_number();
509 int minutes;
510 if (string_pos < string.length() && string[string_pos] == ':') {
511 string_pos++;
512 minutes = parse_number();
513 } else {
514 minutes = hours % 100;
515 hours = hours / 100;
516 }
517
518 tm.tm_hour += sign * hours;
519 tm.tm_min += sign * minutes;
520 break;
521 }
522 case '%':
523 if (string[string_pos] != '%') {
524 return {};
525 }
526 string_pos += 1;
527 break;
528 default:
529 parsing_failed = true;
530 break;
531 }
532
533 if (parsing_failed) {
534 return {};
535 }
536
537 format_pos++;
538 }
539 if (string_pos != string.length() || format_pos != format.length()) {
540 return {};
541 }
542
543 // If an explicit timezone was present, the time in tm was shifted to UTC.
544 // Convert it to local time, since that is what `mktime` expects.
545 if (tm_represents_utc_time) {
546 auto utc_time = timegm(&tm);
547 localtime_r(&utc_time, &tm);
548 }
549
550 return DateTime::from_timestamp(mktime(&tm));
551}
552}