2020-02-11 18:42:02 +00:00
|
|
|
/*
|
2024-10-04 11:19:50 +00:00
|
|
|
* Copyright (c) 2018-2020, Andreas Kling <andreas@ladybird.org>
|
2020-02-11 18:42:02 +00:00
|
|
|
*
|
2021-04-22 08:24:48 +00:00
|
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
2020-02-11 18:42:02 +00:00
|
|
|
*/
|
|
|
|
|
2021-06-01 12:52:38 +00:00
|
|
|
#include <AK/CharacterTypes.h>
|
2022-03-17 01:08:53 +00:00
|
|
|
#include <AK/DateConstants.h>
|
2023-10-30 19:56:43 +00:00
|
|
|
#include <AK/GenericLexer.h>
|
2023-03-27 15:35:24 +00:00
|
|
|
#include <AK/String.h>
|
2020-03-08 10:35:26 +00:00
|
|
|
#include <AK/StringBuilder.h>
|
2020-08-25 20:38:24 +00:00
|
|
|
#include <AK/Time.h>
|
2020-02-11 18:42:02 +00:00
|
|
|
#include <LibCore/DateTime.h>
|
2024-06-25 15:06:08 +00:00
|
|
|
#include <LibUnicode/TimeZone.h>
|
2021-06-01 12:52:38 +00:00
|
|
|
#include <errno.h>
|
2020-02-11 18:42:02 +00:00
|
|
|
#include <time.h>
|
|
|
|
|
2024-10-30 09:35:46 +00:00
|
|
|
#ifdef AK_OS_WINDOWS
|
|
|
|
# define tzname _tzname
|
|
|
|
# define timegm _mkgmtime
|
|
|
|
# define localtime_r(time, tm) localtime_s(tm, time)
|
|
|
|
# define gmtime_r(time, tm) gmtime_s(tm, time)
|
|
|
|
#endif
|
|
|
|
|
2020-02-11 18:42:02 +00:00
|
|
|
namespace Core {
|
|
|
|
|
2024-03-25 14:34:50 +00:00
|
|
|
static Optional<StringView> parse_time_zone_name(GenericLexer& lexer)
|
|
|
|
{
|
2024-06-25 15:06:08 +00:00
|
|
|
auto const& time_zones = Unicode::available_time_zones();
|
2024-03-25 14:34:50 +00:00
|
|
|
auto start_position = lexer.tell();
|
|
|
|
|
|
|
|
Optional<StringView> canonicalized_time_zone;
|
|
|
|
|
|
|
|
lexer.ignore_until([&](auto) {
|
|
|
|
auto time_zone = lexer.input().substring_view(start_position, lexer.tell() - start_position + 1);
|
|
|
|
|
2024-06-25 15:06:08 +00:00
|
|
|
auto it = time_zones.find_if([&](auto const& candidate) { return time_zone.equals_ignoring_ascii_case(candidate); });
|
|
|
|
if (it == time_zones.end())
|
|
|
|
return false;
|
|
|
|
|
|
|
|
canonicalized_time_zone = *it;
|
|
|
|
return true;
|
2024-03-25 14:34:50 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
if (canonicalized_time_zone.has_value())
|
|
|
|
lexer.ignore();
|
|
|
|
|
|
|
|
return canonicalized_time_zone;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void apply_time_zone_offset(StringView time_zone, UnixDateTime& time)
|
|
|
|
{
|
2024-06-25 19:27:20 +00:00
|
|
|
if (auto offset = Unicode::time_zone_offset(time_zone, time); offset.has_value())
|
|
|
|
time -= offset->offset;
|
2024-03-25 14:34:50 +00:00
|
|
|
}
|
2023-11-07 01:50:11 +00:00
|
|
|
|
2020-02-11 18:42:02 +00:00
|
|
|
DateTime DateTime::now()
|
|
|
|
{
|
2020-02-11 18:48:46 +00:00
|
|
|
return from_timestamp(time(nullptr));
|
|
|
|
}
|
|
|
|
|
2021-08-19 16:16:03 +00:00
|
|
|
DateTime DateTime::create(int year, int month, int day, int hour, int minute, int second)
|
2020-03-10 21:41:01 +00:00
|
|
|
{
|
|
|
|
DateTime dt;
|
2020-08-24 13:23:46 +00:00
|
|
|
dt.set_time(year, month, day, hour, minute, second);
|
2020-03-10 21:41:01 +00:00
|
|
|
return dt;
|
|
|
|
}
|
|
|
|
|
2020-02-11 18:48:46 +00:00
|
|
|
DateTime DateTime::from_timestamp(time_t timestamp)
|
|
|
|
{
|
2020-02-11 18:42:02 +00:00
|
|
|
struct tm tm;
|
|
|
|
localtime_r(×tamp, &tm);
|
|
|
|
DateTime dt;
|
|
|
|
dt.m_year = tm.tm_year + 1900;
|
|
|
|
dt.m_month = tm.tm_mon + 1;
|
|
|
|
dt.m_day = tm.tm_mday;
|
|
|
|
dt.m_hour = tm.tm_hour;
|
|
|
|
dt.m_minute = tm.tm_min;
|
|
|
|
dt.m_second = tm.tm_sec;
|
|
|
|
dt.m_timestamp = timestamp;
|
|
|
|
return dt;
|
|
|
|
}
|
|
|
|
|
2020-03-10 21:41:01 +00:00
|
|
|
unsigned DateTime::weekday() const
|
|
|
|
{
|
2020-08-26 00:32:32 +00:00
|
|
|
return ::day_of_week(m_year, m_month, m_day);
|
2020-03-10 21:41:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
unsigned DateTime::days_in_month() const
|
|
|
|
{
|
2020-08-26 00:11:12 +00:00
|
|
|
return ::days_in_month(m_year, m_month);
|
2020-03-10 21:41:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
unsigned DateTime::day_of_year() const
|
|
|
|
{
|
2020-08-25 23:19:16 +00:00
|
|
|
return ::day_of_year(m_year, m_month, m_day);
|
2020-03-10 21:41:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool DateTime::is_leap_year() const
|
|
|
|
{
|
2020-08-25 20:38:24 +00:00
|
|
|
return ::is_leap_year(m_year);
|
2020-03-10 21:41:01 +00:00
|
|
|
}
|
|
|
|
|
2021-08-19 16:16:03 +00:00
|
|
|
void DateTime::set_time(int year, int month, int day, int hour, int minute, int second)
|
2020-03-10 21:41:01 +00:00
|
|
|
{
|
2020-03-23 12:13:36 +00:00
|
|
|
struct tm tm = {};
|
2021-08-19 16:16:03 +00:00
|
|
|
tm.tm_sec = second;
|
|
|
|
tm.tm_min = minute;
|
|
|
|
tm.tm_hour = hour;
|
|
|
|
tm.tm_mday = day;
|
|
|
|
tm.tm_mon = month - 1;
|
|
|
|
tm.tm_year = year - 1900;
|
LibCore: Let DateTime::create()/set_time() take summer time into account
DateTime::create() takes a date/time in local time, but it set
tm_isdst to 0, which meant it was in local winter time always.
Set tm_isdst to -1 so that times during summer time are treated
in summer time, and times in winter time are treated as winter
time (when appropriate). When the time is adjusted backward by
one hour, the same time can be in winter time or summer time,
so this isn't 100% reliable, but for most of the year it should
work fine.
Since LibJS uses DateTime, this means that the Date tuple
ctor (which creates a timestamp from year/month/day/hours/etc
in local time) and getTime() should now have consistent (and
correct) output, which should fix #3327.
In Serenity itself, dst handling (and timezones) are unimplemented
and this doens't have any effect yet, but in Lagom this has an effect.
2020-08-30 13:39:16 +00:00
|
|
|
tm.tm_isdst = -1;
|
2020-08-24 13:24:44 +00:00
|
|
|
// mktime() doesn't read tm.tm_wday and tm.tm_yday, no need to fill them in.
|
|
|
|
|
2020-03-10 21:41:01 +00:00
|
|
|
m_timestamp = mktime(&tm);
|
2020-08-24 13:24:44 +00:00
|
|
|
|
|
|
|
// mktime() normalizes the components to the right ranges (Jan 32 -> Feb 1 etc), so read fields back out from tm.
|
|
|
|
m_year = tm.tm_year + 1900;
|
|
|
|
m_month = tm.tm_mon + 1;
|
|
|
|
m_day = tm.tm_mday;
|
|
|
|
m_hour = tm.tm_hour;
|
|
|
|
m_minute = tm.tm_min;
|
|
|
|
m_second = tm.tm_sec;
|
2020-03-10 21:41:01 +00:00
|
|
|
}
|
|
|
|
|
2023-11-17 18:23:08 +00:00
|
|
|
void DateTime::set_time_only(int hour, int minute, Optional<int> second)
|
|
|
|
{
|
|
|
|
set_time(year(), month(), day(), hour, minute, second.has_value() ? second.release_value() : this->second());
|
|
|
|
}
|
|
|
|
|
2023-11-17 18:23:43 +00:00
|
|
|
void DateTime::set_date(Core::DateTime const& other)
|
|
|
|
{
|
|
|
|
set_time(other.year(), other.month(), other.day(), hour(), minute(), second());
|
|
|
|
}
|
|
|
|
|
2024-04-01 18:45:41 +00:00
|
|
|
ErrorOr<String> DateTime::to_string(StringView format, LocalTime local_time) const
|
2020-02-11 18:42:02 +00:00
|
|
|
{
|
2020-03-08 10:35:26 +00:00
|
|
|
struct tm tm;
|
2024-04-01 18:45:41 +00:00
|
|
|
|
|
|
|
if (local_time == LocalTime::Yes)
|
|
|
|
localtime_r(&m_timestamp, &tm);
|
|
|
|
else
|
|
|
|
gmtime_r(&m_timestamp, &tm);
|
|
|
|
|
2020-03-08 10:35:26 +00:00
|
|
|
StringBuilder builder;
|
2024-04-01 19:21:47 +00:00
|
|
|
size_t const format_len = format.length();
|
2020-03-08 10:35:26 +00:00
|
|
|
|
2023-03-27 15:35:24 +00:00
|
|
|
auto format_time_zone_offset = [&](bool with_separator) -> ErrorOr<void> {
|
2022-08-02 12:42:58 +00:00
|
|
|
struct tm gmt_tm;
|
|
|
|
gmtime_r(&m_timestamp, &gmt_tm);
|
|
|
|
|
|
|
|
gmt_tm.tm_isdst = -1;
|
|
|
|
auto gmt_timestamp = mktime(&gmt_tm);
|
|
|
|
|
|
|
|
auto offset_seconds = static_cast<time_t>(difftime(m_timestamp, gmt_timestamp));
|
2022-01-25 20:46:49 +00:00
|
|
|
StringView offset_sign;
|
|
|
|
|
|
|
|
if (offset_seconds >= 0) {
|
|
|
|
offset_sign = "+"sv;
|
|
|
|
} else {
|
|
|
|
offset_sign = "-"sv;
|
|
|
|
offset_seconds *= -1;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto offset_hours = offset_seconds / 3600;
|
|
|
|
auto offset_minutes = (offset_seconds % 3600) / 60;
|
2022-01-25 21:04:54 +00:00
|
|
|
auto separator = with_separator ? ":"sv : ""sv;
|
2022-01-25 20:46:49 +00:00
|
|
|
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{}{:02}{}{:02}", offset_sign, offset_hours, separator, offset_minutes));
|
|
|
|
return {};
|
2022-01-25 20:46:49 +00:00
|
|
|
};
|
|
|
|
|
2024-04-01 19:21:47 +00:00
|
|
|
for (size_t i = 0; i < format_len; ++i) {
|
2020-03-08 10:35:26 +00:00
|
|
|
if (format[i] != '%') {
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append(format[i]));
|
2020-03-08 10:35:26 +00:00
|
|
|
} else {
|
|
|
|
if (++i == format_len)
|
2023-03-27 15:35:24 +00:00
|
|
|
return String {};
|
2020-03-08 10:35:26 +00:00
|
|
|
|
|
|
|
switch (format[i]) {
|
|
|
|
case 'a':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append(short_day_names[tm.tm_wday]));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'A':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append(long_day_names[tm.tm_wday]));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'b':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append(short_month_names[tm.tm_mon]));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'B':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append(long_month_names[tm.tm_mon]));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'C':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}", (tm.tm_year + 1900) / 100));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'd':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}", tm.tm_mday));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'D':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}/{:02}/{:02}", tm.tm_mon + 1, tm.tm_mday, (tm.tm_year + 1900) % 100));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'e':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:2}", tm.tm_mday));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'h':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append(short_month_names[tm.tm_mon]));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'H':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}", tm.tm_hour));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
2022-04-12 23:53:04 +00:00
|
|
|
case 'I': {
|
|
|
|
int display_hour = tm.tm_hour % 12;
|
|
|
|
if (display_hour == 0)
|
|
|
|
display_hour = 12;
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}", display_hour));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
2022-04-12 23:53:04 +00:00
|
|
|
}
|
2020-03-08 10:35:26 +00:00
|
|
|
case 'j':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:03}", tm.tm_yday + 1));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
2022-11-27 11:42:06 +00:00
|
|
|
case 'l': {
|
|
|
|
int display_hour = tm.tm_hour % 12;
|
|
|
|
if (display_hour == 0)
|
|
|
|
display_hour = 12;
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:2}", display_hour));
|
2022-11-27 11:42:06 +00:00
|
|
|
break;
|
|
|
|
}
|
2020-03-08 10:35:26 +00:00
|
|
|
case 'm':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}", tm.tm_mon + 1));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'M':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}", tm.tm_min));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'n':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append('\n'));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'p':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append(tm.tm_hour < 12 ? "AM"sv : "PM"sv));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
2022-04-12 23:53:04 +00:00
|
|
|
case 'r': {
|
|
|
|
int display_hour = tm.tm_hour % 12;
|
|
|
|
if (display_hour == 0)
|
|
|
|
display_hour = 12;
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}:{:02}:{:02} {}", display_hour, tm.tm_min, tm.tm_sec, tm.tm_hour < 12 ? "AM" : "PM"));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
2022-04-12 23:53:04 +00:00
|
|
|
}
|
2020-03-08 10:35:26 +00:00
|
|
|
case 'R':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}:{:02}", tm.tm_hour, tm.tm_min));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'S':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}", tm.tm_sec));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 't':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append('\t'));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'T':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}:{:02}:{:02}", tm.tm_hour, tm.tm_min, tm.tm_sec));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'u':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{}", tm.tm_wday ? tm.tm_wday : 7));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'U': {
|
2022-04-01 17:58:27 +00:00
|
|
|
int const wday_of_year_beginning = (tm.tm_wday + 6 * tm.tm_yday) % 7;
|
|
|
|
int const week_number = (tm.tm_yday + wday_of_year_beginning) / 7;
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}", week_number));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'V': {
|
2022-04-01 17:58:27 +00:00
|
|
|
int const wday_of_year_beginning = (tm.tm_wday + 6 + 6 * tm.tm_yday) % 7;
|
2020-03-08 10:35:26 +00:00
|
|
|
int week_number = (tm.tm_yday + wday_of_year_beginning) / 7 + 1;
|
|
|
|
if (wday_of_year_beginning > 3) {
|
|
|
|
if (tm.tm_yday >= 7 - wday_of_year_beginning)
|
|
|
|
--week_number;
|
|
|
|
else {
|
2022-04-01 17:58:27 +00:00
|
|
|
int const days_of_last_year = days_in_year(tm.tm_year + 1900 - 1);
|
|
|
|
int const wday_of_last_year_beginning = (wday_of_year_beginning + 6 * days_of_last_year) % 7;
|
2020-03-08 10:35:26 +00:00
|
|
|
week_number = (days_of_last_year + wday_of_last_year_beginning) / 7 + 1;
|
|
|
|
if (wday_of_last_year_beginning > 3)
|
|
|
|
--week_number;
|
|
|
|
}
|
|
|
|
}
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}", week_number));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'w':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{}", tm.tm_wday));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'W': {
|
2022-04-01 17:58:27 +00:00
|
|
|
int const wday_of_year_beginning = (tm.tm_wday + 6 + 6 * tm.tm_yday) % 7;
|
|
|
|
int const week_number = (tm.tm_yday + wday_of_year_beginning) / 7;
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}", week_number));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'y':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{:02}", (tm.tm_year + 1900) % 100));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
case 'Y':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_appendff("{}", tm.tm_year + 1900));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
2022-01-25 20:46:49 +00:00
|
|
|
case 'z':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(format_time_zone_offset(false));
|
2022-01-25 21:04:54 +00:00
|
|
|
break;
|
|
|
|
case ':':
|
2022-04-21 13:31:38 +00:00
|
|
|
if (++i == format_len) {
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append("%:"sv));
|
2022-04-21 13:31:38 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (format[i] != 'z') {
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append("%:"sv));
|
|
|
|
TRY(builder.try_append(format[i]));
|
2022-04-21 13:31:38 +00:00
|
|
|
break;
|
|
|
|
}
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(format_time_zone_offset(true));
|
2022-01-25 20:46:49 +00:00
|
|
|
break;
|
2022-07-11 19:53:29 +00:00
|
|
|
case 'Z': {
|
2022-08-02 12:42:58 +00:00
|
|
|
auto const* timezone_name = tzname[tm.tm_isdst == 0 ? 0 : 1];
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append({ timezone_name, strlen(timezone_name) }));
|
2022-01-25 21:07:22 +00:00
|
|
|
break;
|
2022-07-11 19:53:29 +00:00
|
|
|
}
|
2020-03-08 10:35:26 +00:00
|
|
|
case '%':
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append('%'));
|
2020-03-08 10:35:26 +00:00
|
|
|
break;
|
|
|
|
default:
|
2023-03-27 15:35:24 +00:00
|
|
|
TRY(builder.try_append('%'));
|
|
|
|
TRY(builder.try_append(format[i]));
|
2022-04-21 13:31:38 +00:00
|
|
|
break;
|
2020-03-08 10:35:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-03-27 15:35:24 +00:00
|
|
|
return builder.to_string();
|
|
|
|
}
|
|
|
|
|
2024-04-01 18:45:41 +00:00
|
|
|
ByteString DateTime::to_byte_string(StringView format, LocalTime local_time) const
|
2023-03-27 15:35:24 +00:00
|
|
|
{
|
2024-04-01 18:45:41 +00:00
|
|
|
return MUST(to_string(format, local_time)).to_byte_string();
|
2020-02-11 18:42:02 +00:00
|
|
|
}
|
|
|
|
|
2023-11-07 16:47:23 +00:00
|
|
|
Optional<DateTime> DateTime::parse(StringView format, StringView string)
|
2021-06-01 12:52:38 +00:00
|
|
|
{
|
|
|
|
unsigned format_pos = 0;
|
2023-03-21 15:16:26 +00:00
|
|
|
|
2021-06-01 12:52:38 +00:00
|
|
|
struct tm tm = {};
|
2023-03-21 15:16:26 +00:00
|
|
|
tm.tm_isdst = -1;
|
2021-06-01 12:52:38 +00:00
|
|
|
|
|
|
|
auto parsing_failed = false;
|
2022-12-19 00:44:14 +00:00
|
|
|
auto tm_represents_utc_time = false;
|
2023-11-07 01:50:11 +00:00
|
|
|
Optional<StringView> parsed_time_zone;
|
2021-06-01 12:52:38 +00:00
|
|
|
|
2023-10-30 19:56:43 +00:00
|
|
|
GenericLexer string_lexer(string);
|
|
|
|
|
2021-06-01 12:52:38 +00:00
|
|
|
auto parse_number = [&] {
|
2023-10-30 19:56:43 +00:00
|
|
|
auto result = string_lexer.consume_decimal_integer<int>();
|
|
|
|
if (result.is_error()) {
|
2021-06-01 12:52:38 +00:00
|
|
|
parsing_failed = true;
|
|
|
|
return 0;
|
|
|
|
}
|
2023-10-30 19:56:43 +00:00
|
|
|
return result.value();
|
|
|
|
};
|
2021-06-01 12:52:38 +00:00
|
|
|
|
2023-10-30 19:56:43 +00:00
|
|
|
auto consume = [&](char c) {
|
|
|
|
if (!string_lexer.consume_specific(c))
|
2021-06-01 12:52:38 +00:00
|
|
|
parsing_failed = true;
|
|
|
|
};
|
|
|
|
|
2023-10-30 19:56:43 +00:00
|
|
|
auto consume_specific_ascii_case_insensitive = [&](StringView name) {
|
|
|
|
auto next_string = string_lexer.peek_string(name.length());
|
|
|
|
if (next_string.has_value() && next_string->equals_ignoring_ascii_case(name)) {
|
|
|
|
string_lexer.consume(name.length());
|
|
|
|
return true;
|
2021-06-01 12:52:38 +00:00
|
|
|
}
|
2023-10-30 19:56:43 +00:00
|
|
|
return false;
|
2021-06-01 12:52:38 +00:00
|
|
|
};
|
|
|
|
|
2023-10-30 19:56:43 +00:00
|
|
|
while (format_pos < format.length() && !string_lexer.is_eof()) {
|
2021-06-01 12:52:38 +00:00
|
|
|
if (format[format_pos] != '%') {
|
2023-10-30 19:56:43 +00:00
|
|
|
consume(format[format_pos]);
|
2021-06-01 12:52:38 +00:00
|
|
|
format_pos++;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
format_pos++;
|
2023-10-30 19:56:43 +00:00
|
|
|
if (format_pos == format.length())
|
2021-06-01 12:52:38 +00:00
|
|
|
return {};
|
2023-10-30 19:56:43 +00:00
|
|
|
|
2021-06-01 12:52:38 +00:00
|
|
|
switch (format[format_pos]) {
|
|
|
|
case 'a': {
|
|
|
|
auto wday = 0;
|
2022-03-17 01:08:53 +00:00
|
|
|
for (auto name : short_day_names) {
|
2023-10-30 19:56:43 +00:00
|
|
|
if (consume_specific_ascii_case_insensitive(name)) {
|
2021-06-01 12:52:38 +00:00
|
|
|
tm.tm_wday = wday;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
++wday;
|
|
|
|
}
|
|
|
|
if (wday == 7)
|
|
|
|
return {};
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'A': {
|
|
|
|
auto wday = 0;
|
2022-03-17 01:08:53 +00:00
|
|
|
for (auto name : long_day_names) {
|
2023-10-30 19:56:43 +00:00
|
|
|
if (consume_specific_ascii_case_insensitive(name)) {
|
2021-06-01 12:52:38 +00:00
|
|
|
tm.tm_wday = wday;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
++wday;
|
|
|
|
}
|
|
|
|
if (wday == 7)
|
|
|
|
return {};
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'h':
|
|
|
|
case 'b': {
|
|
|
|
auto mon = 0;
|
2022-03-17 01:08:53 +00:00
|
|
|
for (auto name : short_month_names) {
|
2023-10-30 19:56:43 +00:00
|
|
|
if (consume_specific_ascii_case_insensitive(name)) {
|
2021-06-01 12:52:38 +00:00
|
|
|
tm.tm_mon = mon;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
++mon;
|
|
|
|
}
|
|
|
|
if (mon == 12)
|
|
|
|
return {};
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'B': {
|
|
|
|
auto mon = 0;
|
2022-03-17 01:08:53 +00:00
|
|
|
for (auto name : long_month_names) {
|
2023-10-30 19:56:43 +00:00
|
|
|
if (consume_specific_ascii_case_insensitive(name)) {
|
2021-06-01 12:52:38 +00:00
|
|
|
tm.tm_mon = mon;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
++mon;
|
|
|
|
}
|
|
|
|
if (mon == 12)
|
|
|
|
return {};
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'C': {
|
|
|
|
int num = parse_number();
|
|
|
|
tm.tm_year = (num - 19) * 100;
|
|
|
|
break;
|
|
|
|
}
|
2023-10-30 19:56:43 +00:00
|
|
|
case 'd':
|
2021-06-01 12:52:38 +00:00
|
|
|
tm.tm_mday = parse_number();
|
|
|
|
break;
|
|
|
|
case 'D': {
|
|
|
|
int mon = parse_number();
|
|
|
|
consume('/');
|
|
|
|
int day = parse_number();
|
|
|
|
consume('/');
|
|
|
|
int year = parse_number();
|
|
|
|
tm.tm_mon = mon + 1;
|
|
|
|
tm.tm_mday = day;
|
|
|
|
tm.tm_year = (year + 1900) % 100;
|
|
|
|
break;
|
|
|
|
}
|
2023-10-30 19:56:43 +00:00
|
|
|
case 'e':
|
2021-06-01 12:52:38 +00:00
|
|
|
tm.tm_mday = parse_number();
|
|
|
|
break;
|
2023-10-30 19:56:43 +00:00
|
|
|
case 'H':
|
2021-06-01 12:52:38 +00:00
|
|
|
tm.tm_hour = parse_number();
|
|
|
|
break;
|
|
|
|
case 'I': {
|
|
|
|
int num = parse_number();
|
|
|
|
tm.tm_hour = num % 12;
|
|
|
|
break;
|
|
|
|
}
|
2023-10-30 19:56:43 +00:00
|
|
|
case 'j':
|
2021-06-01 12:52:38 +00:00
|
|
|
// a little trickery here... we can get mktime() to figure out mon and mday using out of range values.
|
|
|
|
// yday is not used so setting it is pointless.
|
|
|
|
tm.tm_mday = parse_number();
|
|
|
|
tm.tm_mon = 0;
|
|
|
|
mktime(&tm);
|
|
|
|
break;
|
|
|
|
case 'm': {
|
|
|
|
int num = parse_number();
|
|
|
|
tm.tm_mon = num - 1;
|
|
|
|
break;
|
|
|
|
}
|
2023-10-30 19:56:43 +00:00
|
|
|
case 'M':
|
2021-06-01 12:52:38 +00:00
|
|
|
tm.tm_min = parse_number();
|
|
|
|
break;
|
|
|
|
case 'n':
|
|
|
|
case 't':
|
2023-10-30 19:56:43 +00:00
|
|
|
string_lexer.consume_while(is_ascii_blank);
|
2021-06-01 12:52:38 +00:00
|
|
|
break;
|
2023-10-30 19:56:43 +00:00
|
|
|
case 'r':
|
2021-06-01 12:52:38 +00:00
|
|
|
case 'p': {
|
2023-10-30 19:56:43 +00:00
|
|
|
auto ampm = string_lexer.consume(2);
|
|
|
|
if (ampm == "PM") {
|
|
|
|
if (tm.tm_hour < 12)
|
|
|
|
tm.tm_hour += 12;
|
|
|
|
} else if (ampm != "AM") {
|
|
|
|
return {};
|
2021-06-01 12:52:38 +00:00
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
2023-10-30 19:56:43 +00:00
|
|
|
case 'R':
|
2021-06-01 12:52:38 +00:00
|
|
|
tm.tm_hour = parse_number();
|
|
|
|
consume(':');
|
|
|
|
tm.tm_min = parse_number();
|
|
|
|
break;
|
|
|
|
case 'S':
|
|
|
|
tm.tm_sec = parse_number();
|
|
|
|
break;
|
|
|
|
case 'T':
|
|
|
|
tm.tm_hour = parse_number();
|
|
|
|
consume(':');
|
|
|
|
tm.tm_min = parse_number();
|
|
|
|
consume(':');
|
|
|
|
tm.tm_sec = parse_number();
|
|
|
|
break;
|
|
|
|
case 'w':
|
|
|
|
tm.tm_wday = parse_number();
|
|
|
|
break;
|
|
|
|
case 'y': {
|
|
|
|
int year = parse_number();
|
|
|
|
tm.tm_year = year <= 99 && year > 69 ? 1900 + year : 2000 + year;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'Y': {
|
|
|
|
int year = parse_number();
|
|
|
|
tm.tm_year = year - 1900;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'z': {
|
2022-12-19 00:44:14 +00:00
|
|
|
tm_represents_utc_time = true;
|
2023-10-30 19:56:43 +00:00
|
|
|
if (string_lexer.consume_specific('Z')) {
|
2021-06-01 12:52:38 +00:00
|
|
|
// UTC time
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
int sign;
|
|
|
|
|
2023-10-30 19:56:43 +00:00
|
|
|
if (string_lexer.consume_specific('+'))
|
2021-06-01 12:52:38 +00:00
|
|
|
sign = -1;
|
2023-10-30 19:56:43 +00:00
|
|
|
else if (string_lexer.consume_specific('-'))
|
2021-06-01 12:52:38 +00:00
|
|
|
sign = +1;
|
|
|
|
else
|
|
|
|
return {};
|
|
|
|
|
|
|
|
auto hours = parse_number();
|
|
|
|
int minutes;
|
2023-10-30 19:56:43 +00:00
|
|
|
if (string_lexer.consume_specific(':')) {
|
2021-06-01 12:52:38 +00:00
|
|
|
minutes = parse_number();
|
|
|
|
} else {
|
|
|
|
minutes = hours % 100;
|
|
|
|
hours = hours / 100;
|
|
|
|
}
|
|
|
|
|
|
|
|
tm.tm_hour += sign * hours;
|
|
|
|
tm.tm_min += sign * minutes;
|
|
|
|
break;
|
|
|
|
}
|
2024-01-29 05:27:35 +00:00
|
|
|
case 'x': {
|
|
|
|
tm_represents_utc_time = true;
|
|
|
|
auto hours = parse_number();
|
|
|
|
int minutes;
|
|
|
|
if (string_lexer.consume_specific(':')) {
|
|
|
|
minutes = parse_number();
|
|
|
|
} else {
|
|
|
|
minutes = hours % 100;
|
|
|
|
hours = hours / 100;
|
|
|
|
}
|
|
|
|
|
|
|
|
tm.tm_hour -= hours;
|
|
|
|
tm.tm_min -= minutes;
|
|
|
|
break;
|
|
|
|
}
|
2024-01-29 05:35:29 +00:00
|
|
|
case 'X': {
|
|
|
|
if (!string_lexer.consume_specific('.'))
|
|
|
|
return {};
|
|
|
|
auto discarded = parse_number();
|
|
|
|
(void)discarded; // NOTE: the tm structure does not support sub second precision, so drop this value.
|
|
|
|
break;
|
|
|
|
}
|
2023-11-07 01:50:11 +00:00
|
|
|
case 'Z':
|
|
|
|
parsed_time_zone = parse_time_zone_name(string_lexer);
|
|
|
|
if (!parsed_time_zone.has_value())
|
|
|
|
return {};
|
|
|
|
|
|
|
|
tm_represents_utc_time = true;
|
|
|
|
break;
|
2023-11-07 14:52:37 +00:00
|
|
|
case '+': {
|
|
|
|
Optional<char> next_format_character;
|
|
|
|
|
|
|
|
if (format_pos + 1 < format.length()) {
|
|
|
|
next_format_character = format[format_pos + 1];
|
|
|
|
|
|
|
|
// Disallow another formatter directly after %+. This is to avoid ambiguity when parsing a string like
|
|
|
|
// "ignoreJan" with "%+%b", as it would be non-trivial to know that where the %b field begins.
|
|
|
|
if (next_format_character == '%')
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
auto discarded = string_lexer.consume_until([&](auto ch) { return ch == next_format_character; });
|
|
|
|
if (discarded.is_empty())
|
|
|
|
return {};
|
|
|
|
|
|
|
|
break;
|
|
|
|
}
|
2021-06-01 12:52:38 +00:00
|
|
|
case '%':
|
2023-10-30 19:56:43 +00:00
|
|
|
consume('%');
|
2021-06-01 12:52:38 +00:00
|
|
|
break;
|
|
|
|
default:
|
|
|
|
parsing_failed = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2023-10-30 19:56:43 +00:00
|
|
|
if (parsing_failed)
|
2021-06-01 12:52:38 +00:00
|
|
|
return {};
|
|
|
|
|
|
|
|
format_pos++;
|
|
|
|
}
|
2023-10-30 19:56:43 +00:00
|
|
|
|
|
|
|
if (!string_lexer.is_eof() || format_pos != format.length())
|
2021-06-01 12:52:38 +00:00
|
|
|
return {};
|
|
|
|
|
2023-11-07 01:50:11 +00:00
|
|
|
// If an explicit time zone offset was present, the time in tm was shifted to UTC. If a time zone name was present,
|
|
|
|
// the time in tm needs to be shifted to UTC. In both cases, convert the result to local time, as that is what is
|
|
|
|
// expected by `mktime`.
|
2022-12-19 00:44:14 +00:00
|
|
|
if (tm_represents_utc_time) {
|
2023-11-07 01:50:11 +00:00
|
|
|
auto utc_time = UnixDateTime::from_seconds_since_epoch(timegm(&tm));
|
|
|
|
|
|
|
|
if (parsed_time_zone.has_value())
|
|
|
|
apply_time_zone_offset(*parsed_time_zone, utc_time);
|
|
|
|
|
|
|
|
time_t utc_time_t = utc_time.seconds_since_epoch();
|
|
|
|
localtime_r(&utc_time_t, &tm);
|
2022-12-19 00:44:14 +00:00
|
|
|
}
|
|
|
|
|
2021-06-01 12:52:38 +00:00
|
|
|
return DateTime::from_timestamp(mktime(&tm));
|
|
|
|
}
|
2023-03-21 15:16:26 +00:00
|
|
|
|
2020-02-11 18:42:02 +00:00
|
|
|
}
|