[libc-commits] [libc] [libc] create TimeReader to look at a struct tm (PR #126138)
Michael Jones via libc-commits
libc-commits at lists.llvm.org
Tue Feb 11 13:04:16 PST 2025
https://github.com/michaelrj-google updated https://github.com/llvm/llvm-project/pull/126138
>From bedf3a00ea13e807d4a67d28bdac853eb21ee3ef Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Thu, 6 Feb 2025 13:47:27 -0800
Subject: [PATCH 1/3] [libc] create TimeReader to look at a struct tm
In the process of adding strftime (#122556) I wrote this utility class
to simplify reading from a struct tm. It provides helper functions that
return basically everything needed by strftime. It's not tested
directly, but it is thoroughly exercised by the strftime tests.
---
libc/include/llvm-libc-types/struct_tm.h | 1 +
libc/src/time/CMakeLists.txt | 2 +
libc/src/time/mktime.cpp | 94 +--------
libc/src/time/time_constants.h | 17 +-
libc/src/time/time_utils.cpp | 92 ++++++++
libc/src/time/time_utils.h | 256 +++++++++++++++++++++++
6 files changed, 368 insertions(+), 94 deletions(-)
diff --git a/libc/include/llvm-libc-types/struct_tm.h b/libc/include/llvm-libc-types/struct_tm.h
index 9fef7c5718ea4..2ec74ecac0293 100644
--- a/libc/include/llvm-libc-types/struct_tm.h
+++ b/libc/include/llvm-libc-types/struct_tm.h
@@ -19,6 +19,7 @@ struct tm {
int tm_wday; // days since Sunday
int tm_yday; // days since January
int tm_isdst; // Daylight Saving Time flag
+ // TODO: add tm_gmtoff and tm_zone? (posix extensions)
};
#endif // LLVM_LIBC_TYPES_STRUCT_TM_H
diff --git a/libc/src/time/CMakeLists.txt b/libc/src/time/CMakeLists.txt
index ef9bfe57bc4ec..dd28aa67280b7 100644
--- a/libc/src/time/CMakeLists.txt
+++ b/libc/src/time/CMakeLists.txt
@@ -22,6 +22,8 @@ add_object_library(
DEPENDS
libc.include.time
libc.src.__support.CPP.limits
+ libc.src.__support.CPP.string_view
+ libc.src.__support.CPP.optional
libc.src.errno.errno
.time_constants
libc.hdr.types.time_t
diff --git a/libc/src/time/mktime.cpp b/libc/src/time/mktime.cpp
index 3874cad02facb..fc05ff2930434 100644
--- a/libc/src/time/mktime.cpp
+++ b/libc/src/time/mktime.cpp
@@ -14,100 +14,8 @@
namespace LIBC_NAMESPACE_DECL {
-// Returns number of years from (1, year).
-static constexpr int64_t get_num_of_leap_years_before(int64_t year) {
- return (year / 4) - (year / 100) + (year / 400);
-}
-
-// Returns True if year is a leap year.
-static constexpr bool is_leap_year(const int64_t year) {
- return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0));
-}
-
LLVM_LIBC_FUNCTION(time_t, mktime, (struct tm * tm_out)) {
- // Unlike most C Library functions, mktime doesn't just die on bad input.
- // TODO(rtenneti); Handle leap seconds.
- int64_t tm_year_from_base = tm_out->tm_year + time_constants::TIME_YEAR_BASE;
-
- // 32-bit end-of-the-world is 03:14:07 UTC on 19 January 2038.
- if (sizeof(time_t) == 4 &&
- tm_year_from_base >= time_constants::END_OF32_BIT_EPOCH_YEAR) {
- if (tm_year_from_base > time_constants::END_OF32_BIT_EPOCH_YEAR)
- return time_utils::out_of_range();
- if (tm_out->tm_mon > 0)
- return time_utils::out_of_range();
- if (tm_out->tm_mday > 19)
- return time_utils::out_of_range();
- else if (tm_out->tm_mday == 19) {
- if (tm_out->tm_hour > 3)
- return time_utils::out_of_range();
- else if (tm_out->tm_hour == 3) {
- if (tm_out->tm_min > 14)
- return time_utils::out_of_range();
- else if (tm_out->tm_min == 14) {
- if (tm_out->tm_sec > 7)
- return time_utils::out_of_range();
- }
- }
- }
- }
-
- // Years are ints. A 32-bit year will fit into a 64-bit time_t.
- // A 64-bit year will not.
- static_assert(
- sizeof(int) == 4,
- "ILP64 is unimplemented. This implementation requires 32-bit integers.");
-
- // Calculate number of months and years from tm_mon.
- int64_t month = tm_out->tm_mon;
- if (month < 0 || month >= time_constants::MONTHS_PER_YEAR - 1) {
- int64_t years = month / 12;
- month %= 12;
- if (month < 0) {
- years--;
- month += 12;
- }
- tm_year_from_base += years;
- }
- bool tm_year_is_leap = is_leap_year(tm_year_from_base);
-
- // Calculate total number of days based on the month and the day (tm_mday).
- int64_t total_days = tm_out->tm_mday - 1;
- for (int64_t i = 0; i < month; ++i)
- total_days += time_constants::NON_LEAP_YEAR_DAYS_IN_MONTH[i];
- // Add one day if it is a leap year and the month is after February.
- if (tm_year_is_leap && month > 1)
- total_days++;
-
- // Calculate total numbers of days based on the year.
- total_days += (tm_year_from_base - time_constants::EPOCH_YEAR) *
- time_constants::DAYS_PER_NON_LEAP_YEAR;
- if (tm_year_from_base >= time_constants::EPOCH_YEAR) {
- total_days += get_num_of_leap_years_before(tm_year_from_base - 1) -
- get_num_of_leap_years_before(time_constants::EPOCH_YEAR);
- } else if (tm_year_from_base >= 1) {
- total_days -= get_num_of_leap_years_before(time_constants::EPOCH_YEAR) -
- get_num_of_leap_years_before(tm_year_from_base - 1);
- } else {
- // Calculate number of leap years until 0th year.
- total_days -= get_num_of_leap_years_before(time_constants::EPOCH_YEAR) -
- get_num_of_leap_years_before(0);
- if (tm_year_from_base <= 0) {
- total_days -= 1; // Subtract 1 for 0th year.
- // Calculate number of leap years until -1 year
- if (tm_year_from_base < 0) {
- total_days -= get_num_of_leap_years_before(-tm_year_from_base) -
- get_num_of_leap_years_before(1);
- }
- }
- }
-
- // TODO: https://github.com/llvm/llvm-project/issues/121962
- // Need to handle timezone and update of tm_isdst.
- int64_t seconds = tm_out->tm_sec +
- tm_out->tm_min * time_constants::SECONDS_PER_MIN +
- tm_out->tm_hour * time_constants::SECONDS_PER_HOUR +
- total_days * time_constants::SECONDS_PER_DAY;
+ int64_t seconds = time_utils::mktime_internal(tm_out);
// Update the tm structure's year, month, day, etc. from seconds.
if (time_utils::update_from_seconds(seconds, tm_out) < 0)
diff --git a/libc/src/time/time_constants.h b/libc/src/time/time_constants.h
index 3e25f741745ab..ab17862fdd957 100644
--- a/libc/src/time/time_constants.h
+++ b/libc/src/time/time_constants.h
@@ -18,7 +18,7 @@ namespace LIBC_NAMESPACE_DECL {
namespace time_constants {
enum Month : int {
- JANUARY,
+ JANUARY = 0,
FEBRUARY,
MARCH,
APRIL,
@@ -32,6 +32,16 @@ enum Month : int {
DECEMBER
};
+enum WeekDay : int {
+ SUNDAY = 0,
+ MONDAY,
+ TUESDAY,
+ WEDNESDAY,
+ THURSDAY,
+ FRIDAY,
+ SATURDAY
+};
+
constexpr int SECONDS_PER_MIN = 60;
constexpr int MINUTES_PER_HOUR = 60;
constexpr int HOURS_PER_DAY = 24;
@@ -40,6 +50,9 @@ constexpr int MONTHS_PER_YEAR = 12;
constexpr int DAYS_PER_NON_LEAP_YEAR = 365;
constexpr int DAYS_PER_LEAP_YEAR = 366;
+constexpr int LAST_DAY_OF_NON_LEAP_YEAR = DAYS_PER_NON_LEAP_YEAR - 1;
+constexpr int LAST_DAY_OF_LEAP_YEAR = DAYS_PER_LEAP_YEAR - 1;
+
constexpr int SECONDS_PER_HOUR = SECONDS_PER_MIN * MINUTES_PER_HOUR;
constexpr int SECONDS_PER_DAY = SECONDS_PER_HOUR * HOURS_PER_DAY;
constexpr int NUMBER_OF_SECONDS_IN_LEAP_YEAR =
@@ -49,6 +62,8 @@ constexpr int TIME_YEAR_BASE = 1900;
constexpr int EPOCH_YEAR = 1970;
constexpr int EPOCH_WEEK_DAY = 4;
+constexpr int ISO_FIRST_DAY_OF_YEAR = 3; // the 4th day of the year, 0-indexed.
+
// For asctime the behavior is undefined if struct tm's tm_wday or tm_mon are
// not within the normal ranges as defined in <time.h>, or if struct tm's
// tm_year exceeds {INT_MAX}-1990, or if the below asctime_internal algorithm
diff --git a/libc/src/time/time_utils.cpp b/libc/src/time/time_utils.cpp
index abc93b8cb961e..585cae0fc33ce 100644
--- a/libc/src/time/time_utils.cpp
+++ b/libc/src/time/time_utils.cpp
@@ -15,6 +15,98 @@
namespace LIBC_NAMESPACE_DECL {
namespace time_utils {
+// TODO: clean this up in a followup patch
+int64_t mktime_internal(const struct tm *tm_out) {
+ // Unlike most C Library functions, mktime doesn't just die on bad input.
+ // TODO(rtenneti); Handle leap seconds.
+ int64_t tm_year_from_base = tm_out->tm_year + time_constants::TIME_YEAR_BASE;
+
+ // 32-bit end-of-the-world is 03:14:07 UTC on 19 January 2038.
+ if (sizeof(time_t) == 4 &&
+ tm_year_from_base >= time_constants::END_OF32_BIT_EPOCH_YEAR) {
+ if (tm_year_from_base > time_constants::END_OF32_BIT_EPOCH_YEAR)
+ return time_utils::out_of_range();
+ if (tm_out->tm_mon > 0)
+ return time_utils::out_of_range();
+ if (tm_out->tm_mday > 19)
+ return time_utils::out_of_range();
+ else if (tm_out->tm_mday == 19) {
+ if (tm_out->tm_hour > 3)
+ return time_utils::out_of_range();
+ else if (tm_out->tm_hour == 3) {
+ if (tm_out->tm_min > 14)
+ return time_utils::out_of_range();
+ else if (tm_out->tm_min == 14) {
+ if (tm_out->tm_sec > 7)
+ return time_utils::out_of_range();
+ }
+ }
+ }
+ }
+
+ // Years are ints. A 32-bit year will fit into a 64-bit time_t.
+ // A 64-bit year will not.
+ static_assert(
+ sizeof(int) == 4,
+ "ILP64 is unimplemented. This implementation requires 32-bit integers.");
+
+ // Calculate number of months and years from tm_mon.
+ int64_t month = tm_out->tm_mon;
+ if (month < 0 || month >= time_constants::MONTHS_PER_YEAR - 1) {
+ int64_t years = month / 12;
+ month %= 12;
+ if (month < 0) {
+ years--;
+ month += 12;
+ }
+ tm_year_from_base += years;
+ }
+ bool tm_year_is_leap = time_utils::is_leap_year(tm_year_from_base);
+
+ // Calculate total number of days based on the month and the day (tm_mday).
+ int64_t total_days = tm_out->tm_mday - 1;
+ for (int64_t i = 0; i < month; ++i)
+ total_days += time_constants::NON_LEAP_YEAR_DAYS_IN_MONTH[i];
+ // Add one day if it is a leap year and the month is after February.
+ if (tm_year_is_leap && month > 1)
+ total_days++;
+
+ // Calculate total numbers of days based on the year.
+ total_days += (tm_year_from_base - time_constants::EPOCH_YEAR) *
+ time_constants::DAYS_PER_NON_LEAP_YEAR;
+ if (tm_year_from_base >= time_constants::EPOCH_YEAR) {
+ total_days +=
+ time_utils::get_num_of_leap_years_before(tm_year_from_base - 1) -
+ time_utils::get_num_of_leap_years_before(time_constants::EPOCH_YEAR);
+ } else if (tm_year_from_base >= 1) {
+ total_days -=
+ time_utils::get_num_of_leap_years_before(time_constants::EPOCH_YEAR) -
+ time_utils::get_num_of_leap_years_before(tm_year_from_base - 1);
+ } else {
+ // Calculate number of leap years until 0th year.
+ total_days -=
+ time_utils::get_num_of_leap_years_before(time_constants::EPOCH_YEAR) -
+ time_utils::get_num_of_leap_years_before(0);
+ if (tm_year_from_base <= 0) {
+ total_days -= 1; // Subtract 1 for 0th year.
+ // Calculate number of leap years until -1 year
+ if (tm_year_from_base < 0) {
+ total_days -=
+ time_utils::get_num_of_leap_years_before(-tm_year_from_base) -
+ time_utils::get_num_of_leap_years_before(1);
+ }
+ }
+ }
+
+ // TODO: https://github.com/llvm/llvm-project/issues/121962
+ // Need to handle timezone and update of tm_isdst.
+ int64_t seconds = tm_out->tm_sec +
+ tm_out->tm_min * time_constants::SECONDS_PER_MIN +
+ tm_out->tm_hour * time_constants::SECONDS_PER_HOUR +
+ total_days * time_constants::SECONDS_PER_DAY;
+ return seconds;
+}
+
static int64_t computeRemainingYears(int64_t daysPerYears,
int64_t quotientYears,
int64_t *remainingDays) {
diff --git a/libc/src/time/time_utils.h b/libc/src/time/time_utils.h
index 5e0a692d4db04..f8521b0e8ec33 100644
--- a/libc/src/time/time_utils.h
+++ b/libc/src/time/time_utils.h
@@ -12,6 +12,8 @@
#include "hdr/types/size_t.h"
#include "hdr/types/struct_tm.h"
#include "hdr/types/time_t.h"
+#include "src/__support/CPP/optional.h"
+#include "src/__support/CPP/string_view.h"
#include "src/__support/common.h"
#include "src/__support/macros/config.h"
#include "src/errno/libc_errno.h"
@@ -22,6 +24,10 @@
namespace LIBC_NAMESPACE_DECL {
namespace time_utils {
+// calculates the seconds from the epoch for tm_in. Does not update the struct,
+// you must call update_from_seconds for that.
+int64_t mktime_internal(const struct tm *tm_out);
+
// Update the "tm" structure's year, month, etc. members from seconds.
// "total_seconds" is the number of seconds since January 1st, 1970.
extern int64_t update_from_seconds(int64_t total_seconds, struct tm *tm);
@@ -61,6 +67,7 @@ LIBC_INLINE char *asctime(const struct tm *timeptr, char *buffer,
}
// TODO(michaelr): move this to use the strftime machinery
+ // equivalent to strftime(buffer, bufferLength, "%a %b %T %Y\n", timeptr)
int written_size = __builtin_snprintf(
buffer, bufferLength, "%.3s %.3s%3d %.2d:%.2d:%.2d %d\n",
time_constants::WEEK_DAY_NAMES[timeptr->tm_wday].data(),
@@ -94,6 +101,255 @@ LIBC_INLINE struct tm *localtime(const time_t *t_ptr) {
return time_utils::gmtime_internal(t_ptr, &result);
}
+// Returns number of years from (1, year).
+LIBC_INLINE constexpr int64_t get_num_of_leap_years_before(int64_t year) {
+ return (year / 4) - (year / 100) + (year / 400);
+}
+
+// Returns True if year is a leap year.
+LIBC_INLINE constexpr bool is_leap_year(const int64_t year) {
+ return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0));
+}
+
+LIBC_INLINE constexpr int get_days_in_year(const int year) {
+ return is_leap_year(year) ? time_constants::DAYS_PER_LEAP_YEAR
+ : time_constants::DAYS_PER_NON_LEAP_YEAR;
+}
+
+// This is a helper class that takes a struct tm and lets you inspect its
+// values. Where relevant, results are bounds checked and returned as optionals.
+// This class does not, however, do data normalization except where necessary.
+// It will faithfully return a date of 9999-99-99, even though that makes no
+// sense.
+class TMReader final {
+ const tm *timeptr;
+
+public:
+ LIBC_INLINE constexpr TMReader(const tm *tmptr) : timeptr(tmptr) { ; }
+
+ // Strings
+ LIBC_INLINE constexpr cpp::optional<cpp::string_view>
+ get_weekday_short_name() const {
+ if (timeptr->tm_wday >= 0 &&
+ timeptr->tm_wday < time_constants::DAYS_PER_WEEK)
+ return time_constants::WEEK_DAY_NAMES[timeptr->tm_wday];
+
+ return cpp::nullopt;
+ }
+
+ LIBC_INLINE constexpr cpp::optional<cpp::string_view>
+ get_weekday_full_name() const {
+ if (timeptr->tm_wday >= 0 &&
+ timeptr->tm_wday < time_constants::DAYS_PER_WEEK)
+ return time_constants::WEEK_DAY_FULL_NAMES[timeptr->tm_wday];
+
+ return cpp::nullopt;
+ }
+
+ LIBC_INLINE constexpr cpp::optional<cpp::string_view>
+ get_month_short_name() const {
+ if (timeptr->tm_mon >= 0 &&
+ timeptr->tm_mon < time_constants::MONTHS_PER_YEAR)
+ return time_constants::MONTH_NAMES[timeptr->tm_mon];
+
+ return cpp::nullopt;
+ }
+
+ LIBC_INLINE constexpr cpp::optional<cpp::string_view>
+ get_month_full_name() const {
+ if (timeptr->tm_mon >= 0 &&
+ timeptr->tm_mon < time_constants::MONTHS_PER_YEAR)
+ return time_constants::MONTH_FULL_NAMES[timeptr->tm_mon];
+
+ return cpp::nullopt;
+ }
+
+ LIBC_INLINE constexpr cpp::string_view get_am_pm() const {
+ if (timeptr->tm_hour < 12)
+ return "AM";
+ return "PM";
+ }
+
+ LIBC_INLINE constexpr cpp::string_view get_timezone_name() const {
+ // TODO: timezone support
+ return "UTC";
+ }
+
+ // Numbers
+ LIBC_INLINE constexpr int get_sec() const { return timeptr->tm_sec; }
+ LIBC_INLINE constexpr int get_min() const { return timeptr->tm_min; }
+ LIBC_INLINE constexpr int get_hour() const { return timeptr->tm_hour; }
+ LIBC_INLINE constexpr int get_mday() const { return timeptr->tm_mday; }
+ LIBC_INLINE constexpr int get_mon() const { return timeptr->tm_mon; }
+ LIBC_INLINE constexpr int get_yday() const { return timeptr->tm_yday; }
+ LIBC_INLINE constexpr int get_wday() const { return timeptr->tm_wday; }
+ LIBC_INLINE constexpr int get_isdst() const { return timeptr->tm_isdst; }
+
+ // returns the year, counting from 1900
+ LIBC_INLINE constexpr int get_year_raw() const { return timeptr->tm_year; }
+ // returns the year, counting from 0
+ LIBC_INLINE constexpr int get_year() const {
+ return timeptr->tm_year + time_constants::TIME_YEAR_BASE;
+ }
+
+ LIBC_INLINE constexpr int is_leap_year() const {
+ return time_utils::is_leap_year(get_year());
+ }
+
+ LIBC_INLINE constexpr int get_iso_wday() const {
+ // ISO uses a week that starts on Monday, but struct tm starts its week on
+ // Sunday. This function normalizes the weekday so that it always returns a
+ // value 0-6
+ const int NORMALIZED_WDAY =
+ timeptr->tm_wday % time_constants::DAYS_PER_WEEK;
+ return (NORMALIZED_WDAY + (time_constants::DAYS_PER_WEEK - 1)) % 7;
+ }
+
+ // returns the week of the current year, with weeks starting on start_day.
+ LIBC_INLINE constexpr int get_week(time_constants::WeekDay start_day) const {
+ // The most recent start_day. The rest of the days into the current week
+ // don't count, so ignore them.
+ // Also add 7 to handle start_day > tm_wday
+ const int start_of_cur_week =
+ timeptr->tm_yday -
+ ((timeptr->tm_wday + time_constants::DAYS_PER_WEEK - start_day) %
+ time_constants::DAYS_PER_WEEK);
+
+ // Add 1 since the first week may start with day 0
+ const int ceil_weeks_since_start =
+ ((start_of_cur_week + 1) + (time_constants::DAYS_PER_WEEK - 1)) /
+ time_constants::DAYS_PER_WEEK;
+ return ceil_weeks_since_start;
+ }
+
+ LIBC_INLINE constexpr int get_iso_week() const {
+ const time_constants::WeekDay start_day = time_constants::MONDAY;
+
+ // The most recent start_day. The rest of the days into the current week
+ // don't count, so ignore them.
+ // Also add 7 to handle start_day > tm_wday
+ const int start_of_cur_week =
+ timeptr->tm_yday -
+ ((timeptr->tm_wday + time_constants::DAYS_PER_WEEK - start_day) %
+ time_constants::DAYS_PER_WEEK);
+
+ // if the week starts in the previous year, and also if the 4th of this year
+ // is not in this week.
+ if (start_of_cur_week < -3) {
+ const int days_into_prev_year =
+ get_days_in_year(get_year() - 1) + start_of_cur_week;
+ // Each year has at least 52 weeks, but a year's last week will be 53 if
+ // its first week starts in the previous year and its last week ends
+ // in the next year. We know get_year() - 1 must extend into get_year(),
+ // so here we check if it also extended into get_year() - 2 and add 1 week
+ // if it does.
+ return 52 + ((days_into_prev_year % time_constants::DAYS_PER_WEEK) >
+ time_constants::ISO_FIRST_DAY_OF_YEAR
+ ? 1
+ : 0);
+ }
+
+ // subtract 1 to account for yday being 0 indexed
+ const int days_until_end_of_year =
+ get_days_in_year(get_year()) - start_of_cur_week - 1;
+
+ // if there are less than 3 days from the start of this week to the end of
+ // the year, then there must be 4 days in this week in the next year, which
+ // means that this week is the first week of that year.
+ if (days_until_end_of_year < 3)
+ return 1;
+
+ // else just calculate the current week like normal.
+ const int ceil_weeks_since_start =
+ ((start_of_cur_week + 1) + (time_constants::DAYS_PER_WEEK - 1)) /
+ time_constants::DAYS_PER_WEEK;
+
+ // add 1 if this year's first week starts in the previous year.
+ return ceil_weeks_since_start +
+ (((start_of_cur_week + time_constants::DAYS_PER_WEEK) %
+ time_constants::DAYS_PER_WEEK) >
+ time_constants::ISO_FIRST_DAY_OF_YEAR
+ ? 1
+ : 0);
+ }
+
+ LIBC_INLINE constexpr int get_iso_year() const {
+ const int BASE_YEAR = get_year();
+ // The ISO year is the same as a standard year for all dates after the start
+ // of the first week and before the last week. Since the first ISO week of a
+ // year starts on the 4th, anything after that is in this year.
+ if (timeptr->tm_yday >= time_constants::ISO_FIRST_DAY_OF_YEAR &&
+ timeptr->tm_yday < time_constants::DAYS_PER_NON_LEAP_YEAR -
+ time_constants::DAYS_PER_WEEK)
+ return BASE_YEAR;
+
+ const int ISO_WDAY = get_iso_wday();
+ // The first week of the ISO year is defined as the week containing the
+ // 4th day of January.
+
+ // first week
+ if (timeptr->tm_yday < time_constants::ISO_FIRST_DAY_OF_YEAR) {
+ /*
+ If jan 4 is in this week, then we're in BASE_YEAR, else we're in the
+ previous year. The formula's been rearranged so here's the derivation:
+
+ +--------+-- days until jan 4
+ | |
+ wday + (4 - yday) < 7
+ | |
+ +---------------+-- weekday of jan 4
+
+ rearranged to get all the constants on one side:
+
+ wday - yday < 7 - 4
+ */
+ return (ISO_WDAY - timeptr->tm_yday <
+ time_constants::DAYS_PER_WEEK -
+ time_constants::ISO_FIRST_DAY_OF_YEAR)
+ ? BASE_YEAR
+ : BASE_YEAR - 1;
+ }
+
+ // last week
+ const int DAYS_LEFT_IN_YEAR =
+ get_days_in_year(get_year()) - timeptr->tm_yday;
+ /*
+ Similar to above, we're checking if jan 4 (of next year) is in this week. If
+ it is, this is in the next year. Note that this also handles the case of
+ yday > days in year gracefully.
+
+ +------------------+-- days until jan 4 (of next year)
+ | |
+ wday + (4 + remaining days) < 7
+ | |
+ +-------------------------+-- weekday of jan 4
+
+ rearranging we get:
+
+ wday + remaining days < 7 - 4
+ */
+
+ return (ISO_WDAY + DAYS_LEFT_IN_YEAR <
+ time_constants::DAYS_PER_WEEK -
+ time_constants::ISO_FIRST_DAY_OF_YEAR)
+ ? BASE_YEAR + 1
+ : BASE_YEAR;
+ }
+
+ LIBC_INLINE constexpr time_t get_epoch() const {
+ return mktime_internal(timeptr);
+ }
+
+ // returns the timezone offset in microwave time:
+ // return (hours * 100) + minutes;
+ // This means that a shift of -4:30 is returned as -430, simplifying
+ // conversion.
+ LIBC_INLINE constexpr int get_timezone_offset() const {
+ // TODO: timezone support
+ return 0;
+ }
+};
+
} // namespace time_utils
} // namespace LIBC_NAMESPACE_DECL
>From d02f1e1784019a2a790c7e77332525e59e36312a Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Tue, 11 Feb 2025 10:26:40 -0800
Subject: [PATCH 2/3] cleanup and address comments
---
libc/src/time/time_constants.h | 1 +
libc/src/time/time_utils.cpp | 6 +-
libc/src/time/time_utils.h | 116 ++++++++++++++++-----------------
3 files changed, 60 insertions(+), 63 deletions(-)
diff --git a/libc/src/time/time_constants.h b/libc/src/time/time_constants.h
index ab17862fdd957..bcf19ff5f193e 100644
--- a/libc/src/time/time_constants.h
+++ b/libc/src/time/time_constants.h
@@ -46,6 +46,7 @@ constexpr int SECONDS_PER_MIN = 60;
constexpr int MINUTES_PER_HOUR = 60;
constexpr int HOURS_PER_DAY = 24;
constexpr int DAYS_PER_WEEK = 7;
+constexpr int WEEKS_PER_YEAR = 52;
constexpr int MONTHS_PER_YEAR = 12;
constexpr int DAYS_PER_NON_LEAP_YEAR = 365;
constexpr int DAYS_PER_LEAP_YEAR = 366;
diff --git a/libc/src/time/time_utils.cpp b/libc/src/time/time_utils.cpp
index 585cae0fc33ce..3ccb2dd934967 100644
--- a/libc/src/time/time_utils.cpp
+++ b/libc/src/time/time_utils.cpp
@@ -12,11 +12,13 @@
#include "src/__support/macros/config.h"
#include "src/time/time_constants.h"
+#include <stdint.h>
+
namespace LIBC_NAMESPACE_DECL {
namespace time_utils {
// TODO: clean this up in a followup patch
-int64_t mktime_internal(const struct tm *tm_out) {
+int64_t mktime_internal(const tm *tm_out) {
// Unlike most C Library functions, mktime doesn't just die on bad input.
// TODO(rtenneti); Handle leap seconds.
int64_t tm_year_from_base = tm_out->tm_year + time_constants::TIME_YEAR_BASE;
@@ -134,7 +136,7 @@ static int64_t computeRemainingYears(int64_t daysPerYears,
//
// Compute the number of months from the remaining days. Finally, adjust years
// to be 1900 and months to be from January.
-int64_t update_from_seconds(int64_t total_seconds, struct tm *tm) {
+int64_t update_from_seconds(int64_t total_seconds, tm *tm) {
// Days in month starting from March in the year 2000.
static const char daysInMonth[] = {31 /* Mar */, 30, 31, 30, 31, 31,
30, 31, 30, 31, 31, 29};
diff --git a/libc/src/time/time_utils.h b/libc/src/time/time_utils.h
index f8521b0e8ec33..de8dd497f7d5a 100644
--- a/libc/src/time/time_utils.h
+++ b/libc/src/time/time_utils.h
@@ -26,11 +26,11 @@ namespace time_utils {
// calculates the seconds from the epoch for tm_in. Does not update the struct,
// you must call update_from_seconds for that.
-int64_t mktime_internal(const struct tm *tm_out);
+int64_t mktime_internal(const tm *tm_out);
// Update the "tm" structure's year, month, etc. members from seconds.
// "total_seconds" is the number of seconds since January 1st, 1970.
-extern int64_t update_from_seconds(int64_t total_seconds, struct tm *tm);
+extern int64_t update_from_seconds(int64_t total_seconds, tm *tm);
// TODO(michaelrj): move these functions to use ErrorOr instead of setting
// errno. They always accompany a specific return value so we only need the one
@@ -49,7 +49,7 @@ LIBC_INLINE time_t out_of_range() {
LIBC_INLINE void invalid_value() { libc_errno = EINVAL; }
-LIBC_INLINE char *asctime(const struct tm *timeptr, char *buffer,
+LIBC_INLINE char *asctime(const tm *timeptr, char *buffer,
size_t bufferLength) {
if (timeptr == nullptr || buffer == nullptr) {
invalid_value();
@@ -83,7 +83,7 @@ LIBC_INLINE char *asctime(const struct tm *timeptr, char *buffer,
return buffer;
}
-LIBC_INLINE struct tm *gmtime_internal(const time_t *timer, struct tm *result) {
+LIBC_INLINE tm *gmtime_internal(const time_t *timer, tm *result) {
int64_t seconds = *timer;
// Update the tm structure's year, month, day, etc. from seconds.
if (update_from_seconds(seconds, result) < 0) {
@@ -96,8 +96,8 @@ LIBC_INLINE struct tm *gmtime_internal(const time_t *timer, struct tm *result) {
// TODO: localtime is not yet implemented and a temporary solution is to
// use gmtime, https://github.com/llvm/llvm-project/issues/107597
-LIBC_INLINE struct tm *localtime(const time_t *t_ptr) {
- static struct tm result;
+LIBC_INLINE tm *localtime(const time_t *t_ptr) {
+ static tm result;
return time_utils::gmtime_internal(t_ptr, &result);
}
@@ -124,44 +124,38 @@ LIBC_INLINE constexpr int get_days_in_year(const int year) {
class TMReader final {
const tm *timeptr;
+ template <size_t N>
+ LIBC_INLINE constexpr cpp::optional<cpp::string_view>
+ bounds_check(const cpp::array<cpp::string_view, N> &arr, int index) const {
+ if (index >= 0 && index < static_cast<int>(arr.size()))
+ return arr[index];
+ return cpp::nullopt;
+ }
+
public:
- LIBC_INLINE constexpr TMReader(const tm *tmptr) : timeptr(tmptr) { ; }
+ LIBC_INLINE constexpr explicit TMReader(const tm *tmptr) : timeptr(tmptr) {
+ ;
+ }
// Strings
LIBC_INLINE constexpr cpp::optional<cpp::string_view>
get_weekday_short_name() const {
- if (timeptr->tm_wday >= 0 &&
- timeptr->tm_wday < time_constants::DAYS_PER_WEEK)
- return time_constants::WEEK_DAY_NAMES[timeptr->tm_wday];
-
- return cpp::nullopt;
+ return bounds_check(time_constants::WEEK_DAY_NAMES, timeptr->tm_wday);
}
LIBC_INLINE constexpr cpp::optional<cpp::string_view>
get_weekday_full_name() const {
- if (timeptr->tm_wday >= 0 &&
- timeptr->tm_wday < time_constants::DAYS_PER_WEEK)
- return time_constants::WEEK_DAY_FULL_NAMES[timeptr->tm_wday];
-
- return cpp::nullopt;
+ return bounds_check(time_constants::WEEK_DAY_FULL_NAMES, timeptr->tm_wday);
}
LIBC_INLINE constexpr cpp::optional<cpp::string_view>
get_month_short_name() const {
- if (timeptr->tm_mon >= 0 &&
- timeptr->tm_mon < time_constants::MONTHS_PER_YEAR)
- return time_constants::MONTH_NAMES[timeptr->tm_mon];
-
- return cpp::nullopt;
+ return bounds_check(time_constants::MONTH_NAMES, timeptr->tm_mon);
}
LIBC_INLINE constexpr cpp::optional<cpp::string_view>
get_month_full_name() const {
- if (timeptr->tm_mon >= 0 &&
- timeptr->tm_mon < time_constants::MONTHS_PER_YEAR)
- return time_constants::MONTH_FULL_NAMES[timeptr->tm_mon];
-
- return cpp::nullopt;
+ return bounds_check(time_constants::MONTH_FULL_NAMES, timeptr->tm_mon);
}
LIBC_INLINE constexpr cpp::string_view get_am_pm() const {
@@ -197,41 +191,49 @@ class TMReader final {
}
LIBC_INLINE constexpr int get_iso_wday() const {
+ using time_constants::DAYS_PER_WEEK;
+ using time_constants::MONDAY;
// ISO uses a week that starts on Monday, but struct tm starts its week on
// Sunday. This function normalizes the weekday so that it always returns a
// value 0-6
- const int NORMALIZED_WDAY =
- timeptr->tm_wday % time_constants::DAYS_PER_WEEK;
- return (NORMALIZED_WDAY + (time_constants::DAYS_PER_WEEK - 1)) % 7;
+ const int NORMALIZED_WDAY = timeptr->tm_wday % DAYS_PER_WEEK;
+ return (NORMALIZED_WDAY + (DAYS_PER_WEEK - MONDAY)) % DAYS_PER_WEEK;
}
// returns the week of the current year, with weeks starting on start_day.
LIBC_INLINE constexpr int get_week(time_constants::WeekDay start_day) const {
+ using time_constants::DAYS_PER_WEEK;
// The most recent start_day. The rest of the days into the current week
// don't count, so ignore them.
// Also add 7 to handle start_day > tm_wday
const int start_of_cur_week =
timeptr->tm_yday -
- ((timeptr->tm_wday + time_constants::DAYS_PER_WEEK - start_day) %
- time_constants::DAYS_PER_WEEK);
+ ((timeptr->tm_wday + DAYS_PER_WEEK - start_day) % DAYS_PER_WEEK);
- // Add 1 since the first week may start with day 0
+ // The original formula is ceil((start_of_cur_week + 1) / DAYS_PER_WEEK)
+ // That becomes (start_of_cur_week + 1 + DAYS_PER_WEEK - 1) / DAYS_PER_WEEK)
+ // Which simplifies to (start_of_cur_week + DAYS_PER_WEEK) / DAYS_PER_WEEK
const int ceil_weeks_since_start =
- ((start_of_cur_week + 1) + (time_constants::DAYS_PER_WEEK - 1)) /
- time_constants::DAYS_PER_WEEK;
+ (start_of_cur_week + DAYS_PER_WEEK) / DAYS_PER_WEEK;
+
return ceil_weeks_since_start;
}
LIBC_INLINE constexpr int get_iso_week() const {
- const time_constants::WeekDay start_day = time_constants::MONDAY;
+ using time_constants::DAYS_PER_WEEK;
+ using time_constants::ISO_FIRST_DAY_OF_YEAR;
+ using time_constants::MONDAY;
+ using time_constants::WeekDay;
+ using time_constants::WEEKS_PER_YEAR;
+
+ constexpr WeekDay START_DAY = MONDAY;
// The most recent start_day. The rest of the days into the current week
// don't count, so ignore them.
// Also add 7 to handle start_day > tm_wday
const int start_of_cur_week =
timeptr->tm_yday -
- ((timeptr->tm_wday + time_constants::DAYS_PER_WEEK - start_day) %
- time_constants::DAYS_PER_WEEK);
+ ((timeptr->tm_wday + DAYS_PER_WEEK - START_DAY) % DAYS_PER_WEEK);
// if the week starts in the previous year, and also if the 4th of this year
// is not in this week.
@@ -243,10 +245,8 @@ class TMReader final {
// in the next year. We know get_year() - 1 must extend into get_year(),
// so here we check if it also extended into get_year() - 2 and add 1 week
// if it does.
- return 52 + ((days_into_prev_year % time_constants::DAYS_PER_WEEK) >
- time_constants::ISO_FIRST_DAY_OF_YEAR
- ? 1
- : 0);
+ return WEEKS_PER_YEAR +
+ ((days_into_prev_year % DAYS_PER_WEEK) > ISO_FIRST_DAY_OF_YEAR);
}
// subtract 1 to account for yday being 0 indexed
@@ -261,16 +261,13 @@ class TMReader final {
// else just calculate the current week like normal.
const int ceil_weeks_since_start =
- ((start_of_cur_week + 1) + (time_constants::DAYS_PER_WEEK - 1)) /
- time_constants::DAYS_PER_WEEK;
+ (start_of_cur_week + DAYS_PER_WEEK) / DAYS_PER_WEEK;
// add 1 if this year's first week starts in the previous year.
- return ceil_weeks_since_start +
- (((start_of_cur_week + time_constants::DAYS_PER_WEEK) %
- time_constants::DAYS_PER_WEEK) >
- time_constants::ISO_FIRST_DAY_OF_YEAR
- ? 1
- : 0);
+ const int WEEK_STARTS_IN_PREV_YEAR =
+ ((start_of_cur_week + time_constants::DAYS_PER_WEEK) %
+ time_constants::DAYS_PER_WEEK) > time_constants::ISO_FIRST_DAY_OF_YEAR;
+ return ceil_weeks_since_start + WEEK_STARTS_IN_PREV_YEAR;
}
LIBC_INLINE constexpr int get_iso_year() const {
@@ -303,11 +300,10 @@ class TMReader final {
wday - yday < 7 - 4
*/
- return (ISO_WDAY - timeptr->tm_yday <
- time_constants::DAYS_PER_WEEK -
- time_constants::ISO_FIRST_DAY_OF_YEAR)
- ? BASE_YEAR
- : BASE_YEAR - 1;
+ const int IS_CUR_YEAR = (ISO_WDAY - timeptr->tm_yday <
+ time_constants::DAYS_PER_WEEK -
+ time_constants::ISO_FIRST_DAY_OF_YEAR);
+ return BASE_YEAR - !IS_CUR_YEAR;
}
// last week
@@ -328,12 +324,10 @@ class TMReader final {
wday + remaining days < 7 - 4
*/
-
- return (ISO_WDAY + DAYS_LEFT_IN_YEAR <
- time_constants::DAYS_PER_WEEK -
- time_constants::ISO_FIRST_DAY_OF_YEAR)
- ? BASE_YEAR + 1
- : BASE_YEAR;
+ const int IS_NEXT_YEAR =
+ (ISO_WDAY + DAYS_LEFT_IN_YEAR <
+ time_constants::DAYS_PER_WEEK - time_constants::ISO_FIRST_DAY_OF_YEAR);
+ return BASE_YEAR + IS_NEXT_YEAR;
}
LIBC_INLINE constexpr time_t get_epoch() const {
>From 41b560e3e0f55fbbc4b02ee371b4fac324f256c1 Mon Sep 17 00:00:00 2001
From: Michael Jones <michaelrj at google.com>
Date: Tue, 11 Feb 2025 13:02:48 -0800
Subject: [PATCH 3/3] address comments
---
libc/src/time/time_utils.h | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/libc/src/time/time_utils.h b/libc/src/time/time_utils.h
index de8dd497f7d5a..30cdcbc170639 100644
--- a/libc/src/time/time_utils.h
+++ b/libc/src/time/time_utils.h
@@ -30,7 +30,7 @@ int64_t mktime_internal(const tm *tm_out);
// Update the "tm" structure's year, month, etc. members from seconds.
// "total_seconds" is the number of seconds since January 1st, 1970.
-extern int64_t update_from_seconds(int64_t total_seconds, tm *tm);
+int64_t update_from_seconds(int64_t total_seconds, tm *tm);
// TODO(michaelrj): move these functions to use ErrorOr instead of setting
// errno. They always accompany a specific return value so we only need the one
@@ -133,9 +133,7 @@ class TMReader final {
}
public:
- LIBC_INLINE constexpr explicit TMReader(const tm *tmptr) : timeptr(tmptr) {
- ;
- }
+ LIBC_INLINE constexpr explicit TMReader(const tm *tmptr) : timeptr(tmptr) {}
// Strings
LIBC_INLINE constexpr cpp::optional<cpp::string_view>
More information about the libc-commits
mailing list