diff --git a/AK/Time.cpp b/AK/Time.cpp index 5acf0740a01..a31952bd005 100644 --- a/AK/Time.cpp +++ b/AK/Time.cpp @@ -6,10 +6,15 @@ */ #include +#include +#include +#include #include #ifdef AK_OS_WINDOWS # include +# define localtime_r(time, tm) localtime_s(tm, time) +# define gmtime_r(time, tm) gmtime_s(tm, time) #endif namespace AK { @@ -304,4 +309,168 @@ UnixDateTime UnixDateTime::now_coarse() return UnixDateTime { now_time_from_clock(CLOCK_REALTIME_COARSE) }; } +ErrorOr UnixDateTime::to_string(StringView format, LocalTime local_time) const +{ + struct tm tm; + + auto timestamp = m_offset.to_timespec().tv_sec; + if (local_time == LocalTime::Yes) + (void)localtime_r(×tamp, &tm); + else + (void)gmtime_r(×tamp, &tm); + + StringBuilder builder; + size_t const format_len = format.length(); + + for (size_t i = 0; i < format_len; ++i) { + if (format[i] != '%') { + TRY(builder.try_append(format[i])); + } else { + if (++i == format_len) + return String {}; + + switch (format[i]) { + case 'a': + TRY(builder.try_append(short_day_names[tm.tm_wday])); + break; + case 'A': + TRY(builder.try_append(long_day_names[tm.tm_wday])); + break; + case 'b': + TRY(builder.try_append(short_month_names[tm.tm_mon])); + break; + case 'B': + TRY(builder.try_append(long_month_names[tm.tm_mon])); + break; + case 'C': + TRY(builder.try_appendff("{:02}", (tm.tm_year + 1900) / 100)); + break; + case 'd': + TRY(builder.try_appendff("{:02}", tm.tm_mday)); + break; + case 'D': + TRY(builder.try_appendff("{:02}/{:02}/{:02}", tm.tm_mon + 1, tm.tm_mday, (tm.tm_year + 1900) % 100)); + break; + case 'e': + TRY(builder.try_appendff("{:2}", tm.tm_mday)); + break; + case 'h': + TRY(builder.try_append(short_month_names[tm.tm_mon])); + break; + case 'H': + TRY(builder.try_appendff("{:02}", tm.tm_hour)); + break; + case 'I': { + int display_hour = tm.tm_hour % 12; + if (display_hour == 0) + display_hour = 12; + TRY(builder.try_appendff("{:02}", display_hour)); + break; + } + case 'j': + TRY(builder.try_appendff("{:03}", tm.tm_yday + 1)); + break; + case 'l': { + int display_hour = tm.tm_hour % 12; + if (display_hour == 0) + display_hour = 12; + TRY(builder.try_appendff("{:2}", display_hour)); + break; + } + case 'm': + TRY(builder.try_appendff("{:02}", tm.tm_mon + 1)); + break; + case 'M': + TRY(builder.try_appendff("{:02}", tm.tm_min)); + break; + case 'n': + TRY(builder.try_append('\n')); + break; + case 'p': + TRY(builder.try_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; + TRY(builder.try_appendff("{:02}:{:02}:{:02} {}", display_hour, tm.tm_min, tm.tm_sec, tm.tm_hour < 12 ? "AM" : "PM")); + break; + } + case 'R': + TRY(builder.try_appendff("{:02}:{:02}", tm.tm_hour, tm.tm_min)); + break; + case 'S': + TRY(builder.try_appendff("{:02}", tm.tm_sec)); + break; + case 't': + TRY(builder.try_append('\t')); + break; + case 'T': + TRY(builder.try_appendff("{:02}:{:02}:{:02}", tm.tm_hour, tm.tm_min, tm.tm_sec)); + break; + case 'u': + TRY(builder.try_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; + TRY(builder.try_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; + } + } + TRY(builder.try_appendff("{:02}", week_number)); + break; + } + case 'w': + TRY(builder.try_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; + TRY(builder.try_appendff("{:02}", week_number)); + break; + } + case 'y': + TRY(builder.try_appendff("{:02}", (tm.tm_year + 1900) % 100)); + break; + case 'Y': + TRY(builder.try_appendff("{}", tm.tm_year + 1900)); + break; + case 'Z': { + auto const* timezone_name = tzname[tm.tm_isdst == 0 ? 0 : 1]; + TRY(builder.try_append({ timezone_name, strlen(timezone_name) })); + break; + } + case '%': + TRY(builder.try_append('%')); + break; + default: + TRY(builder.try_append('%')); + TRY(builder.try_append(format[i])); + break; + } + } + } + + return builder.to_string(); +} + +ByteString UnixDateTime::to_byte_string(StringView format, LocalTime local_time) const +{ + return MUST(to_string(format, local_time)).to_byte_string(); +} + } diff --git a/AK/Time.h b/AK/Time.h index b7c2ed32553..4d566fbe87d 100644 --- a/AK/Time.h +++ b/AK/Time.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #ifdef AK_OS_WINDOWS // https://learn.microsoft.com/en-us/windows/win32/api/winsock/ns-winsock-timeval @@ -438,6 +439,41 @@ public: // Never returns a point after this UnixDateTime, since fractional seconds are cut off. [[nodiscard]] i64 truncated_seconds_since_epoch() const { return m_offset.to_truncated_seconds(); } + enum class LocalTime { + Yes, + No, + }; + + // %a: require short day name + // %A: require long day name + // %h/b: require short month name + // %B: require long month name + // %C: require short year number (hundreds) (ex: 19 -> 1900) + // %d: require day number + // %D: require month number/day number/short year number (ex: 12/31/24) + // %e: require day number + // %H: require hour (24h format) + // %I: require hour (12h format) + // %j: require defining date with day number ? (not sure) + // %m: require set to month entred - 1 + // %M: require minutes + // %n: require newline + // %t: require tab + // %r/p: require AM | PM + // %R: require hours:minutes (ex: 13:57) + // %S: require seconds + // %T: require hours:minutes:seconds (ex: 13:57:34) + // %w: require week day number + // %y: require 2 digits year (69 < year < 99: in the 1900 else in 2000) + // %Y: require full year + // %x: require single number to represent hour and minutes + // %X: require sub second precision + // %Z: require timezone name + // %+: ignore until next '%' + // %%: require character '%' + ErrorOr to_string(StringView format = "%Y-%m-%d %H:%M:%S"sv, LocalTime = LocalTime::Yes) const; + ByteString to_byte_string(StringView format = "%Y-%m-%d %H:%M:%S"sv, LocalTime = LocalTime::Yes) const; + // Offsetting a UNIX time by a duration yields another UNIX time. constexpr UnixDateTime operator+(Duration const& other) const { return UnixDateTime { m_offset + other }; } constexpr UnixDateTime& operator+=(Duration const& other) diff --git a/Tests/AK/CMakeLists.txt b/Tests/AK/CMakeLists.txt index 4d7ab411a38..a2548024e4b 100644 --- a/Tests/AK/CMakeLists.txt +++ b/Tests/AK/CMakeLists.txt @@ -21,7 +21,6 @@ set(AK_TEST_SOURCES TestDisjointChunks.cpp TestDistinctNumeric.cpp TestDoublyLinkedList.cpp - TestDuration.cpp TestEndian.cpp TestEnumBits.cpp TestEnumerate.cpp @@ -71,6 +70,7 @@ set(AK_TEST_SOURCES TestStringFloatingPointConversions.cpp TestStringUtils.cpp TestStringView.cpp + TestTime.cpp TestTrie.cpp TestTuple.cpp TestTypeTraits.cpp diff --git a/Tests/AK/TestDuration.cpp b/Tests/AK/TestTime.cpp similarity index 96% rename from Tests/AK/TestDuration.cpp rename to Tests/AK/TestTime.cpp index f9b7952dc19..9ce3b8ad4fb 100644 --- a/Tests/AK/TestDuration.cpp +++ b/Tests/AK/TestTime.cpp @@ -647,3 +647,42 @@ TEST_CASE(from_unix_time_parts_overflow) EXPECT_DURATION(UnixDateTime::from_unix_time_parts(2'147'483'647, 12, 255, 255, 255, 255, 65535).offset_to_epoch(), 67767976253733620, 535'000'000); EXPECT_DURATION(UnixDateTime::from_unix_time_parts(2'147'483'647, 255, 255, 255, 255, 255, 65535).offset_to_epoch(), 67767976202930420, 535'000'000); } + +TEST_CASE(time_to_string) +{ + auto test = [](auto format, auto expected, i32 year, u8 month, u8 day, u8 hour, u8 minute, u8 second) { + auto result = AK::UnixDateTime::from_unix_time_parts(year, month, day, hour, minute, second, 0).to_string(format, AK::UnixDateTime::LocalTime::No); + VERIFY(!result.is_error()); + + EXPECT_EQ(expected, result.value()); + }; + + test("%Y/%m/%d %R"sv, "2023/01/23 10:50"sv, 2023, 1, 23, 10, 50, 10); + + // two-digit year and century + test("%y %C"sv, "23 20"sv, 2023, 1, 23, 10, 50, 10); + + // zero- and space-padded day, and %D shortcut + test("%d %e"sv, "05 5"sv, 2023, 1, 5, 0, 0, 0); + test("%D"sv, "01/23/23"sv, 2023, 1, 23, 0, 0, 0); + + // full time and seconds + test("%T"sv, "10:50:10"sv, 2023, 1, 23, 10, 50, 10); + test("%S"sv, "05"sv, 2023, 1, 1, 0, 0, 5); + + // 12-hour clock with AM/PM + test("%H %I %p"sv, "00 12 AM"sv, 2023, 1, 5, 0, 0, 0); + test("%H %I %p"sv, "15 03 PM"sv, 2023, 1, 5, 15, 0, 0); + + // short/long weekday and month names + test("%a %A"sv, "Mon Monday"sv, 2023, 1, 23, 0, 0, 0); + test("%b %B"sv, "Jan January"sv, 2023, 1, 5, 0, 0, 0); + + // numeric weekday and day‐of‐year + test("%w %j"sv, "1 023"sv, 2023, 1, 23, 0, 0, 0); + + // newline, tab and literal '%' + test("%n"sv, "\n"sv, 2023, 1, 1, 0, 0, 0); + test("%t"sv, "\t"sv, 2023, 1, 1, 0, 0, 0); + test("%%"sv, "%"sv, 2023, 1, 1, 0, 0, 0); +}