| //===-- 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<time_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. |
| 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<size_t>(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 <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 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 |