mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-24 18:24:45 -05:00
6e7459322d
Having an alias function that only wraps another one is silly, and keeping the more obvious name should flush out more uses of deprecated strings. No behavior change.
552 lines
16 KiB
C++
552 lines
16 KiB
C++
/*
|
|
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <AK/CharacterTypes.h>
|
|
#include <AK/DateConstants.h>
|
|
#include <AK/StringBuilder.h>
|
|
#include <AK/Time.h>
|
|
#include <LibCore/DateTime.h>
|
|
#include <errno.h>
|
|
#include <time.h>
|
|
|
|
namespace Core {
|
|
|
|
DateTime DateTime::now()
|
|
{
|
|
return from_timestamp(time(nullptr));
|
|
}
|
|
|
|
DateTime DateTime::create(int year, int month, int day, int hour, int minute, int second)
|
|
{
|
|
DateTime dt;
|
|
dt.set_time(year, month, day, hour, minute, second);
|
|
return dt;
|
|
}
|
|
|
|
DateTime DateTime::from_timestamp(time_t timestamp)
|
|
{
|
|
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;
|
|
}
|
|
|
|
unsigned DateTime::weekday() const
|
|
{
|
|
return ::day_of_week(m_year, m_month, m_day);
|
|
}
|
|
|
|
unsigned DateTime::days_in_month() const
|
|
{
|
|
return ::days_in_month(m_year, m_month);
|
|
}
|
|
|
|
unsigned DateTime::day_of_year() const
|
|
{
|
|
return ::day_of_year(m_year, m_month, m_day);
|
|
}
|
|
|
|
bool DateTime::is_leap_year() const
|
|
{
|
|
return ::is_leap_year(m_year);
|
|
}
|
|
|
|
void DateTime::set_time(int year, int month, int day, int hour, int minute, int second)
|
|
{
|
|
struct tm tm = {};
|
|
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;
|
|
tm.tm_isdst = -1;
|
|
// mktime() doesn't read tm.tm_wday and tm.tm_yday, no need to fill them in.
|
|
|
|
m_timestamp = mktime(&tm);
|
|
|
|
// 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;
|
|
}
|
|
|
|
DeprecatedString DateTime::to_deprecated_string(StringView format) const
|
|
{
|
|
struct tm tm;
|
|
localtime_r(&m_timestamp, &tm);
|
|
StringBuilder builder;
|
|
int const format_len = format.length();
|
|
|
|
auto format_time_zone_offset = [&](bool with_separator) {
|
|
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));
|
|
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;
|
|
auto separator = with_separator ? ":"sv : ""sv;
|
|
|
|
builder.appendff("{}{:02}{}{:02}", offset_sign, offset_hours, separator, offset_minutes);
|
|
};
|
|
|
|
for (int i = 0; i < format_len; ++i) {
|
|
if (format[i] != '%') {
|
|
builder.append(format[i]);
|
|
} else {
|
|
if (++i == format_len)
|
|
return DeprecatedString();
|
|
|
|
switch (format[i]) {
|
|
case 'a':
|
|
builder.append(short_day_names[tm.tm_wday]);
|
|
break;
|
|
case 'A':
|
|
builder.append(long_day_names[tm.tm_wday]);
|
|
break;
|
|
case 'b':
|
|
builder.append(short_month_names[tm.tm_mon]);
|
|
break;
|
|
case 'B':
|
|
builder.append(long_month_names[tm.tm_mon]);
|
|
break;
|
|
case 'C':
|
|
builder.appendff("{:02}", (tm.tm_year + 1900) / 100);
|
|
break;
|
|
case 'd':
|
|
builder.appendff("{:02}", tm.tm_mday);
|
|
break;
|
|
case 'D':
|
|
builder.appendff("{:02}/{:02}/{:02}", tm.tm_mon + 1, tm.tm_mday, (tm.tm_year + 1900) % 100);
|
|
break;
|
|
case 'e':
|
|
builder.appendff("{:2}", tm.tm_mday);
|
|
break;
|
|
case 'h':
|
|
builder.append(short_month_names[tm.tm_mon]);
|
|
break;
|
|
case 'H':
|
|
builder.appendff("{:02}", tm.tm_hour);
|
|
break;
|
|
case 'I': {
|
|
int display_hour = tm.tm_hour % 12;
|
|
if (display_hour == 0)
|
|
display_hour = 12;
|
|
builder.appendff("{:02}", display_hour);
|
|
break;
|
|
}
|
|
case 'j':
|
|
builder.appendff("{:03}", tm.tm_yday + 1);
|
|
break;
|
|
case 'l': {
|
|
int display_hour = tm.tm_hour % 12;
|
|
if (display_hour == 0)
|
|
display_hour = 12;
|
|
builder.appendff("{:2}", display_hour);
|
|
break;
|
|
}
|
|
case 'm':
|
|
builder.appendff("{:02}", tm.tm_mon + 1);
|
|
break;
|
|
case 'M':
|
|
builder.appendff("{:02}", tm.tm_min);
|
|
break;
|
|
case 'n':
|
|
builder.append('\n');
|
|
break;
|
|
case 'p':
|
|
builder.append(tm.tm_hour < 12 ? "AM"sv : "PM"sv);
|
|
break;
|
|
case 'r': {
|
|
int display_hour = tm.tm_hour % 12;
|
|
if (display_hour == 0)
|
|
display_hour = 12;
|
|
builder.appendff("{:02}:{:02}:{:02} {}", display_hour, tm.tm_min, tm.tm_sec, tm.tm_hour < 12 ? "AM" : "PM");
|
|
break;
|
|
}
|
|
case 'R':
|
|
builder.appendff("{:02}:{:02}", tm.tm_hour, tm.tm_min);
|
|
break;
|
|
case 'S':
|
|
builder.appendff("{:02}", tm.tm_sec);
|
|
break;
|
|
case 't':
|
|
builder.append('\t');
|
|
break;
|
|
case 'T':
|
|
builder.appendff("{:02}:{:02}:{:02}", tm.tm_hour, tm.tm_min, tm.tm_sec);
|
|
break;
|
|
case 'u':
|
|
builder.appendff("{}", tm.tm_wday ? tm.tm_wday : 7);
|
|
break;
|
|
case 'U': {
|
|
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;
|
|
builder.appendff("{:02}", week_number);
|
|
break;
|
|
}
|
|
case 'V': {
|
|
int const wday_of_year_beginning = (tm.tm_wday + 6 + 6 * tm.tm_yday) % 7;
|
|
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 {
|
|
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;
|
|
week_number = (days_of_last_year + wday_of_last_year_beginning) / 7 + 1;
|
|
if (wday_of_last_year_beginning > 3)
|
|
--week_number;
|
|
}
|
|
}
|
|
builder.appendff("{:02}", week_number);
|
|
break;
|
|
}
|
|
case 'w':
|
|
builder.appendff("{}", tm.tm_wday);
|
|
break;
|
|
case 'W': {
|
|
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;
|
|
builder.appendff("{:02}", week_number);
|
|
break;
|
|
}
|
|
case 'y':
|
|
builder.appendff("{:02}", (tm.tm_year + 1900) % 100);
|
|
break;
|
|
case 'Y':
|
|
builder.appendff("{}", tm.tm_year + 1900);
|
|
break;
|
|
case 'z':
|
|
format_time_zone_offset(false);
|
|
break;
|
|
case ':':
|
|
if (++i == format_len) {
|
|
builder.append("%:"sv);
|
|
break;
|
|
}
|
|
if (format[i] != 'z') {
|
|
builder.append("%:"sv);
|
|
builder.append(format[i]);
|
|
break;
|
|
}
|
|
format_time_zone_offset(true);
|
|
break;
|
|
case 'Z': {
|
|
auto const* timezone_name = tzname[tm.tm_isdst == 0 ? 0 : 1];
|
|
builder.append({ timezone_name, strlen(timezone_name) });
|
|
break;
|
|
}
|
|
case '%':
|
|
builder.append('%');
|
|
break;
|
|
default:
|
|
builder.append('%');
|
|
builder.append(format[i]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return builder.to_deprecated_string();
|
|
}
|
|
|
|
Optional<DateTime> DateTime::parse(StringView format, DeprecatedString const& string)
|
|
{
|
|
unsigned format_pos = 0;
|
|
unsigned string_pos = 0;
|
|
struct tm tm = {};
|
|
|
|
auto parsing_failed = false;
|
|
auto tm_represents_utc_time = false;
|
|
|
|
auto parse_number = [&] {
|
|
if (string_pos >= string.length()) {
|
|
parsing_failed = true;
|
|
return 0;
|
|
}
|
|
|
|
char* end_ptr = nullptr;
|
|
errno = 0;
|
|
int number = strtol(string.characters() + string_pos, &end_ptr, 10);
|
|
|
|
auto chars_parsed = end_ptr - (string.characters() + string_pos);
|
|
if (chars_parsed == 0 || errno != 0)
|
|
parsing_failed = true;
|
|
else
|
|
string_pos += chars_parsed;
|
|
return number;
|
|
};
|
|
|
|
auto consume = [&](char x) {
|
|
if (string_pos >= string.length()) {
|
|
parsing_failed = true;
|
|
return;
|
|
}
|
|
if (string[string_pos] != x)
|
|
parsing_failed = true;
|
|
else
|
|
string_pos++;
|
|
};
|
|
|
|
while (format_pos < format.length() && string_pos < string.length()) {
|
|
if (format[format_pos] != '%') {
|
|
if (format[format_pos] != string[string_pos]) {
|
|
return {};
|
|
}
|
|
format_pos++;
|
|
string_pos++;
|
|
continue;
|
|
}
|
|
|
|
format_pos++;
|
|
if (format_pos == format.length()) {
|
|
return {};
|
|
}
|
|
switch (format[format_pos]) {
|
|
case 'a': {
|
|
auto wday = 0;
|
|
for (auto name : short_day_names) {
|
|
if (string.substring_view(string_pos).starts_with(name, AK::CaseSensitivity::CaseInsensitive)) {
|
|
string_pos += name.length();
|
|
tm.tm_wday = wday;
|
|
break;
|
|
}
|
|
++wday;
|
|
}
|
|
if (wday == 7)
|
|
return {};
|
|
break;
|
|
}
|
|
case 'A': {
|
|
auto wday = 0;
|
|
for (auto name : long_day_names) {
|
|
if (string.substring_view(string_pos).starts_with(name, AK::CaseSensitivity::CaseInsensitive)) {
|
|
string_pos += name.length();
|
|
tm.tm_wday = wday;
|
|
break;
|
|
}
|
|
++wday;
|
|
}
|
|
if (wday == 7)
|
|
return {};
|
|
break;
|
|
}
|
|
case 'h':
|
|
case 'b': {
|
|
auto mon = 0;
|
|
for (auto name : short_month_names) {
|
|
if (string.substring_view(string_pos).starts_with(name, AK::CaseSensitivity::CaseInsensitive)) {
|
|
string_pos += name.length();
|
|
tm.tm_mon = mon;
|
|
break;
|
|
}
|
|
++mon;
|
|
}
|
|
if (mon == 12)
|
|
return {};
|
|
break;
|
|
}
|
|
case 'B': {
|
|
auto mon = 0;
|
|
for (auto name : long_month_names) {
|
|
if (string.substring_view(string_pos).starts_with(name, AK::CaseSensitivity::CaseInsensitive)) {
|
|
string_pos += name.length();
|
|
tm.tm_mon = mon;
|
|
break;
|
|
}
|
|
++mon;
|
|
}
|
|
if (mon == 12)
|
|
return {};
|
|
break;
|
|
}
|
|
case 'C': {
|
|
int num = parse_number();
|
|
tm.tm_year = (num - 19) * 100;
|
|
break;
|
|
}
|
|
case 'd': {
|
|
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;
|
|
}
|
|
case 'e': {
|
|
tm.tm_mday = parse_number();
|
|
break;
|
|
}
|
|
case 'H': {
|
|
tm.tm_hour = parse_number();
|
|
break;
|
|
}
|
|
case 'I': {
|
|
int num = parse_number();
|
|
tm.tm_hour = num % 12;
|
|
break;
|
|
}
|
|
case 'j': {
|
|
// 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;
|
|
}
|
|
case 'M': {
|
|
tm.tm_min = parse_number();
|
|
break;
|
|
}
|
|
case 'n':
|
|
case 't':
|
|
while (is_ascii_blank(string[string_pos])) {
|
|
string_pos++;
|
|
}
|
|
break;
|
|
case 'p': {
|
|
auto ampm = string.substring_view(string_pos, 2);
|
|
if (ampm == "PM" && tm.tm_hour < 12) {
|
|
tm.tm_hour += 12;
|
|
}
|
|
string_pos += 2;
|
|
break;
|
|
}
|
|
case 'r': {
|
|
auto ampm = string.substring_view(string_pos, 2);
|
|
if (ampm == "PM" && tm.tm_hour < 12) {
|
|
tm.tm_hour += 12;
|
|
}
|
|
string_pos += 2;
|
|
break;
|
|
}
|
|
case 'R': {
|
|
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': {
|
|
tm_represents_utc_time = true;
|
|
if (string[string_pos] == 'Z') {
|
|
// UTC time
|
|
string_pos++;
|
|
break;
|
|
}
|
|
int sign;
|
|
|
|
if (string[string_pos] == '+')
|
|
sign = -1;
|
|
else if (string[string_pos] == '-')
|
|
sign = +1;
|
|
else
|
|
return {};
|
|
|
|
string_pos++;
|
|
|
|
auto hours = parse_number();
|
|
int minutes;
|
|
if (string_pos < string.length() && string[string_pos] == ':') {
|
|
string_pos++;
|
|
minutes = parse_number();
|
|
} else {
|
|
minutes = hours % 100;
|
|
hours = hours / 100;
|
|
}
|
|
|
|
tm.tm_hour += sign * hours;
|
|
tm.tm_min += sign * minutes;
|
|
break;
|
|
}
|
|
case '%':
|
|
if (string[string_pos] != '%') {
|
|
return {};
|
|
}
|
|
string_pos += 1;
|
|
break;
|
|
default:
|
|
parsing_failed = true;
|
|
break;
|
|
}
|
|
|
|
if (parsing_failed) {
|
|
return {};
|
|
}
|
|
|
|
format_pos++;
|
|
}
|
|
if (string_pos != string.length() || format_pos != format.length()) {
|
|
return {};
|
|
}
|
|
|
|
// If an explicit timezone was present, the time in tm was shifted to UTC.
|
|
// Convert it to local time, since that is what `mktime` expects.
|
|
if (tm_represents_utc_time) {
|
|
auto utc_time = timegm(&tm);
|
|
localtime_r(&utc_time, &tm);
|
|
}
|
|
|
|
return DateTime::from_timestamp(mktime(&tm));
|
|
}
|
|
}
|