| //===----------------------------------------------------------------------===// | |
| // | |
| // 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 | |
| // | |
| //===----------------------------------------------------------------------===// | |
| /// | |
| /// \file | |
| /// Collection of utils for mktime and friends. | |
| /// | |
| //===----------------------------------------------------------------------===// | |
| #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/error_or.h" | |
| #include "src/__support/libc_errno.h" | |
| #include "src/__support/macros/config.h" | |
| #include "src/time/time_constants.h" | |
| namespace LIBC_NAMESPACE_DECL { | |
| namespace time_utils { | |
| /// Calculates the seconds from the epoch for tm_in. | |
| /// Does not update the struct, call update_from_seconds for that. | |
| /// | |
| /// \param tm_out Pointer to the tm structure to read. | |
| /// \return The epoch time on success, or nullopt on failure. | |
| cpp::optional<time_t> mktime_internal(const tm *tm_out); | |
| // EOVERFLOW is a POSIX extension and might not be defined on targets that only | |
| // support the C standard (e.g. generic baremetal). The C standard does not | |
| // require setting errno on overflow for time functions, but POSIX does. | |
| // We map it to ERANGE on non-POSIX targets to provide consistent error | |
| // reporting while remaining compliant. | |
| #ifdef EOVERFLOW | |
| constexpr int TIME_OVERFLOW = EOVERFLOW; | |
| #else | |
| constexpr int TIME_OVERFLOW = ERANGE; | |
| #endif | |
| /// Update the "tm" structure's year, month, etc. members from seconds. | |
| /// | |
| /// \param total_seconds The number of seconds since January 1st, 1970. | |
| /// \param tm Pointer to the tm structure to update. | |
| /// \return void on success, or error code on failure. | |
| ErrorOr<void> update_from_seconds(time_t total_seconds, tm *tm); | |
| LIBC_INLINE ErrorOr<tm *> gmtime_internal(const time_t *timer, tm *result) { | |
| time_t seconds = *timer; | |
| auto status = update_from_seconds(seconds, result); | |
| if (!status) { | |
| return cpp::unexpected(status.error()); | |
| } | |
| #if defined(__linux__) | |
| result->tm_gmtoff = 0; | |
| result->tm_zone = "GMT"; | |
| #endif | |
| return result; | |
| } | |
| LIBC_INLINE ErrorOr<tm *> localtime_internal(const time_t *timer, tm *result) { | |
| time_t seconds = *timer; | |
| auto status = update_from_seconds(seconds, result); | |
| if (!status) { | |
| return cpp::unexpected(status.error()); | |
| } | |
| // TODO(zimirza): implement timezone database | |
| #if defined(__linux__) | |
| result->tm_gmtoff = 0; | |
| result->tm_zone = "UTC"; | |
| #endif | |
| return result; | |
| } | |
| LIBC_INLINE ErrorOr<tm *> localtime(const time_t *t_ptr) { | |
| if (t_ptr == nullptr) | |
| return cpp::unexpected(EINVAL); | |
| static tm result; | |
| return time_utils::localtime_internal(t_ptr, &result); | |
| } | |
| // Returns True if year is a leap year. | |
| // Uses the Drepper-Neri-Schneider algorithm (version 3). | |
| // https://www.benjoffe.com/fast-leap-year#drepper-neri-schneider | |
| // | |
| // % 25 is faster than % 100 on modern compilers. False positives (years | |
| // divisible by 25 but not 100) are harmless: the & 15 check only differs | |
| // from & 3 when the year truly is a century year, and non-century years | |
| // that happen to be divisible by 25 always pass both checks identically. | |
| LIBC_INLINE constexpr bool is_leap_year(const int64_t year) { | |
| const bool is_mul_25 = (year % 25 == 0); | |
| return (year & (is_mul_25 ? 15 : 3)) == 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. | |
| /// Class to read fields from tm structure. | |
| /// | |
| /// 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"; | |
| } | |
| /// Get timezone name. | |
| /// | |
| /// \return Timezone name string, or empty string if not set. | |
| LIBC_INLINE constexpr cpp::string_view get_timezone_name() const { | |
| #if defined(__linux__) | |
| return timeptr->tm_zone ? timeptr->tm_zone : ""; | |
| #else | |
| return "UTC"; | |
| #endif | |
| } | |
| // 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; | |
| } | |
| /// Get epoch time. | |
| /// | |
| /// \return Epoch time in seconds on success, or TIME_OVERFLOW on failure. | |
| LIBC_INLINE ErrorOr<time_t> get_epoch() const { | |
| auto seconds = mktime_internal(timeptr); | |
| if (!seconds) | |
| return cpp::unexpected(TIME_OVERFLOW); | |
| return *seconds; | |
| } | |
| /// Get timezone offset. | |
| /// | |
| /// The offset is returned in microwave time: (hours * 100) + minutes. | |
| /// For example, -04:30 is returned as -430. | |
| /// | |
| /// \return Timezone offset in microwave time. | |
| LIBC_INLINE constexpr int get_timezone_offset() const { | |
| #if defined(__linux__) | |
| // TODO: This relies on tm_gmtoff which is currently initialized to 0 | |
| // by localtime/gmtime until timezone database is implemented. | |
| int64_t seconds = timeptr->tm_gmtoff; | |
| int64_t hours = seconds / 3600; | |
| int64_t minutes = (seconds % 3600) / 60; | |
| return static_cast<int>((hours * 100) + minutes); | |
| #else | |
| return 0; | |
| #endif | |
| } | |
| }; | |
| } // namespace time_utils | |
| } // namespace LIBC_NAMESPACE_DECL | |
| #endif // LLVM_LIBC_SRC_TIME_TIME_UTILS_H |