//===-- Collection of utils for mktime and friends --------------*- C++ -*-===// // // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #ifndef LLVM_LIBC_SRC_TIME_TIME_UTILS_H #define LLVM_LIBC_SRC_TIME_TIME_UTILS_H #include "hdr/stdint_proxy.h" #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/libc_errno.h" #include "src/__support/macros/config.h" #include "time_constants.h" 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. cpp::optional 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. int64_t update_from_seconds(time_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 // variable. // POSIX.1-2017 requires this. LIBC_INLINE time_t out_of_range() { #ifdef EOVERFLOW // For non-POSIX uses of the standard C time functions, where EOVERFLOW is // not defined, it's OK not to set errno at all. The plain C standard doesn't // require it. libc_errno = EOVERFLOW; #endif return time_constants::OUT_OF_RANGE_RETURN_VALUE; } LIBC_INLINE void invalid_value() { libc_errno = EINVAL; } LIBC_INLINE char *asctime(const tm *timeptr, char *buffer, size_t bufferLength) { if (timeptr == nullptr || buffer == nullptr) { invalid_value(); return nullptr; } if (timeptr->tm_wday < 0 || timeptr->tm_wday > (time_constants::DAYS_PER_WEEK - 1)) { invalid_value(); return nullptr; } if (timeptr->tm_mon < 0 || timeptr->tm_mon > (time_constants::MONTHS_PER_YEAR - 1)) { invalid_value(); return nullptr; } // 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(), time_constants::MONTH_NAMES[timeptr->tm_mon].data(), timeptr->tm_mday, timeptr->tm_hour, timeptr->tm_min, timeptr->tm_sec, time_constants::TIME_YEAR_BASE + timeptr->tm_year); if (written_size < 0) return nullptr; if (static_cast(written_size) >= bufferLength) { out_of_range(); return nullptr; } return buffer; } LIBC_INLINE tm *gmtime_internal(const time_t *timer, tm *result) { time_t seconds = *timer; // Update the tm structure's year, month, day, etc. from seconds. if (update_from_seconds(seconds, result) < 0) { out_of_range(); return nullptr; } return 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 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 LIBC_INLINE constexpr cpp::optional bounds_check(const cpp::array &arr, int index) const { if (index >= 0 && index < static_cast(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 get_weekday_short_name() const { return bounds_check(time_constants::WEEK_DAY_NAMES, timeptr->tm_wday); } LIBC_INLINE constexpr cpp::optional get_weekday_full_name() const { return bounds_check(time_constants::WEEK_DAY_FULL_NAMES, timeptr->tm_wday); } LIBC_INLINE constexpr cpp::optional get_month_short_name() const { return bounds_check(time_constants::MONTH_NAMES, timeptr->tm_mon); } LIBC_INLINE constexpr cpp::optional 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 time_t get_epoch() const { auto seconds = mktime_internal(timeptr); return seconds ? *seconds : time_utils::out_of_range(); } // 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 #endif // LLVM_LIBC_SRC_TIME_TIME_UTILS_H