[libc] create TimeReader to look at a struct tm (#126138)

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.
This commit is contained in:
Michael Jones
2025-02-11 14:37:15 -08:00
committed by GitHub
parent ad6cd7e8b2
commit a760e7faac
6 changed files with 369 additions and 100 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -18,7 +18,7 @@ namespace LIBC_NAMESPACE_DECL {
namespace time_constants {
enum Month : int {
JANUARY,
JANUARY = 0,
FEBRUARY,
MARCH,
APRIL,
@@ -32,14 +32,28 @@ 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;
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;
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 +63,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

View File

@@ -12,9 +12,103 @@
#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 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) {
@@ -42,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};

View File

@@ -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,9 +24,13 @@
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 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);
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
@@ -43,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();
@@ -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(),
@@ -76,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) {
@@ -89,11 +96,252 @@ 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);
}
// 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;
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 explicit TMReader(const tm *tmptr) : timeptr(tmptr) {}
// Strings
LIBC_INLINE constexpr cpp::optional<cpp::string_view>
get_weekday_short_name() const {
return bounds_check(time_constants::WEEK_DAY_NAMES, timeptr->tm_wday);
}
LIBC_INLINE constexpr cpp::optional<cpp::string_view>
get_weekday_full_name() const {
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 {
return bounds_check(time_constants::MONTH_NAMES, timeptr->tm_mon);
}
LIBC_INLINE constexpr cpp::optional<cpp::string_view>
get_month_full_name() const {
return bounds_check(time_constants::MONTH_FULL_NAMES, timeptr->tm_mon);
}
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 {
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 % 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 + DAYS_PER_WEEK - start_day) % DAYS_PER_WEEK);
// 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 + DAYS_PER_WEEK) / DAYS_PER_WEEK;
return ceil_weeks_since_start;
}
LIBC_INLINE constexpr int get_iso_week() const {
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 + 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.
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 WEEKS_PER_YEAR +
((days_into_prev_year % DAYS_PER_WEEK) > ISO_FIRST_DAY_OF_YEAR);
}
// 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 + DAYS_PER_WEEK) / DAYS_PER_WEEK;
// add 1 if this year's first week starts in the previous year.
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 {
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
*/
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
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
*/
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 {
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