360 lines · c
1//===-- Collection of utils for mktime and friends --------------*- C++ -*-===//2//3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.4// See https://llvm.org/LICENSE.txt for license information.5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception6//7//===----------------------------------------------------------------------===//8 9#ifndef LLVM_LIBC_SRC_TIME_TIME_UTILS_H10#define LLVM_LIBC_SRC_TIME_TIME_UTILS_H11 12#include "hdr/stdint_proxy.h"13#include "hdr/types/size_t.h"14#include "hdr/types/struct_tm.h"15#include "hdr/types/time_t.h"16#include "src/__support/CPP/optional.h"17#include "src/__support/CPP/string_view.h"18#include "src/__support/common.h"19#include "src/__support/libc_errno.h"20#include "src/__support/macros/config.h"21#include "time_constants.h"22 23namespace LIBC_NAMESPACE_DECL {24namespace time_utils {25 26// calculates the seconds from the epoch for tm_in. Does not update the struct,27// you must call update_from_seconds for that.28cpp::optional<time_t> mktime_internal(const tm *tm_out);29 30// Update the "tm" structure's year, month, etc. members from seconds.31// "total_seconds" is the number of seconds since January 1st, 1970.32int64_t update_from_seconds(time_t total_seconds, tm *tm);33 34// TODO(michaelrj): move these functions to use ErrorOr instead of setting35// errno. They always accompany a specific return value so we only need the one36// variable.37 38// POSIX.1-2017 requires this.39LIBC_INLINE time_t out_of_range() {40#ifdef EOVERFLOW41 // For non-POSIX uses of the standard C time functions, where EOVERFLOW is42 // not defined, it's OK not to set errno at all. The plain C standard doesn't43 // require it.44 libc_errno = EOVERFLOW;45#endif46 return time_constants::OUT_OF_RANGE_RETURN_VALUE;47}48 49LIBC_INLINE void invalid_value() { libc_errno = EINVAL; }50 51LIBC_INLINE char *asctime(const tm *timeptr, char *buffer,52 size_t bufferLength) {53 if (timeptr == nullptr || buffer == nullptr) {54 invalid_value();55 return nullptr;56 }57 if (timeptr->tm_wday < 0 ||58 timeptr->tm_wday > (time_constants::DAYS_PER_WEEK - 1)) {59 invalid_value();60 return nullptr;61 }62 if (timeptr->tm_mon < 0 ||63 timeptr->tm_mon > (time_constants::MONTHS_PER_YEAR - 1)) {64 invalid_value();65 return nullptr;66 }67 68 // TODO(michaelr): move this to use the strftime machinery69 // equivalent to strftime(buffer, bufferLength, "%a %b %T %Y\n", timeptr)70 int written_size = __builtin_snprintf(71 buffer, bufferLength, "%.3s %.3s%3d %.2d:%.2d:%.2d %d\n",72 time_constants::WEEK_DAY_NAMES[timeptr->tm_wday].data(),73 time_constants::MONTH_NAMES[timeptr->tm_mon].data(), timeptr->tm_mday,74 timeptr->tm_hour, timeptr->tm_min, timeptr->tm_sec,75 time_constants::TIME_YEAR_BASE + timeptr->tm_year);76 if (written_size < 0)77 return nullptr;78 if (static_cast<size_t>(written_size) >= bufferLength) {79 out_of_range();80 return nullptr;81 }82 return buffer;83}84 85LIBC_INLINE tm *gmtime_internal(const time_t *timer, tm *result) {86 time_t seconds = *timer;87 // Update the tm structure's year, month, day, etc. from seconds.88 if (update_from_seconds(seconds, result) < 0) {89 out_of_range();90 return nullptr;91 }92 93 return result;94}95 96LIBC_INLINE tm *localtime_internal(const time_t *timer, tm *result) {97 time_t seconds = *timer;98 // Update the tm structure's year, month, day, etc. from seconds.99 if (update_from_seconds(seconds, result) < 0) {100 out_of_range();101 return nullptr;102 }103 104 // TODO(zimirza): implement timezone database105 106 return result;107}108 109LIBC_INLINE tm *localtime(const time_t *t_ptr) {110 static tm result;111 return time_utils::localtime_internal(t_ptr, &result);112}113 114// Returns number of years from (1, year).115LIBC_INLINE constexpr int64_t get_num_of_leap_years_before(int64_t year) {116 return (year / 4) - (year / 100) + (year / 400);117}118 119// Returns True if year is a leap year.120LIBC_INLINE constexpr bool is_leap_year(const int64_t year) {121 return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0));122}123 124LIBC_INLINE constexpr int get_days_in_year(const int year) {125 return is_leap_year(year) ? time_constants::DAYS_PER_LEAP_YEAR126 : time_constants::DAYS_PER_NON_LEAP_YEAR;127}128 129// This is a helper class that takes a struct tm and lets you inspect its130// values. Where relevant, results are bounds checked and returned as optionals.131// This class does not, however, do data normalization except where necessary.132// It will faithfully return a date of 9999-99-99, even though that makes no133// sense.134class TMReader final {135 const tm *timeptr;136 137 template <size_t N>138 LIBC_INLINE constexpr cpp::optional<cpp::string_view>139 bounds_check(const cpp::array<cpp::string_view, N> &arr, int index) const {140 if (index >= 0 && index < static_cast<int>(arr.size()))141 return arr[index];142 return cpp::nullopt;143 }144 145public:146 LIBC_INLINE constexpr explicit TMReader(const tm *tmptr) : timeptr(tmptr) {}147 148 // Strings149 LIBC_INLINE constexpr cpp::optional<cpp::string_view>150 get_weekday_short_name() const {151 return bounds_check(time_constants::WEEK_DAY_NAMES, timeptr->tm_wday);152 }153 154 LIBC_INLINE constexpr cpp::optional<cpp::string_view>155 get_weekday_full_name() const {156 return bounds_check(time_constants::WEEK_DAY_FULL_NAMES, timeptr->tm_wday);157 }158 159 LIBC_INLINE constexpr cpp::optional<cpp::string_view>160 get_month_short_name() const {161 return bounds_check(time_constants::MONTH_NAMES, timeptr->tm_mon);162 }163 164 LIBC_INLINE constexpr cpp::optional<cpp::string_view>165 get_month_full_name() const {166 return bounds_check(time_constants::MONTH_FULL_NAMES, timeptr->tm_mon);167 }168 169 LIBC_INLINE constexpr cpp::string_view get_am_pm() const {170 if (timeptr->tm_hour < 12)171 return "AM";172 return "PM";173 }174 175 LIBC_INLINE constexpr cpp::string_view get_timezone_name() const {176 // TODO: timezone support177 return "UTC";178 }179 180 // Numbers181 LIBC_INLINE constexpr int get_sec() const { return timeptr->tm_sec; }182 LIBC_INLINE constexpr int get_min() const { return timeptr->tm_min; }183 LIBC_INLINE constexpr int get_hour() const { return timeptr->tm_hour; }184 LIBC_INLINE constexpr int get_mday() const { return timeptr->tm_mday; }185 LIBC_INLINE constexpr int get_mon() const { return timeptr->tm_mon; }186 LIBC_INLINE constexpr int get_yday() const { return timeptr->tm_yday; }187 LIBC_INLINE constexpr int get_wday() const { return timeptr->tm_wday; }188 LIBC_INLINE constexpr int get_isdst() const { return timeptr->tm_isdst; }189 190 // returns the year, counting from 1900191 LIBC_INLINE constexpr int get_year_raw() const { return timeptr->tm_year; }192 // returns the year, counting from 0193 LIBC_INLINE constexpr int get_year() const {194 return timeptr->tm_year + time_constants::TIME_YEAR_BASE;195 }196 197 LIBC_INLINE constexpr int is_leap_year() const {198 return time_utils::is_leap_year(get_year());199 }200 201 LIBC_INLINE constexpr int get_iso_wday() const {202 using time_constants::DAYS_PER_WEEK;203 using time_constants::MONDAY;204 // ISO uses a week that starts on Monday, but struct tm starts its week on205 // Sunday. This function normalizes the weekday so that it always returns a206 // value 0-6207 const int NORMALIZED_WDAY = timeptr->tm_wday % DAYS_PER_WEEK;208 return (NORMALIZED_WDAY + (DAYS_PER_WEEK - MONDAY)) % DAYS_PER_WEEK;209 }210 211 // returns the week of the current year, with weeks starting on start_day.212 LIBC_INLINE constexpr int get_week(time_constants::WeekDay start_day) const {213 using time_constants::DAYS_PER_WEEK;214 // The most recent start_day. The rest of the days into the current week215 // don't count, so ignore them.216 // Also add 7 to handle start_day > tm_wday217 const int start_of_cur_week =218 timeptr->tm_yday -219 ((timeptr->tm_wday + DAYS_PER_WEEK - start_day) % DAYS_PER_WEEK);220 221 // The original formula is ceil((start_of_cur_week + 1) / DAYS_PER_WEEK)222 // That becomes (start_of_cur_week + 1 + DAYS_PER_WEEK - 1) / DAYS_PER_WEEK)223 // Which simplifies to (start_of_cur_week + DAYS_PER_WEEK) / DAYS_PER_WEEK224 const int ceil_weeks_since_start =225 (start_of_cur_week + DAYS_PER_WEEK) / DAYS_PER_WEEK;226 227 return ceil_weeks_since_start;228 }229 230 LIBC_INLINE constexpr int get_iso_week() const {231 using time_constants::DAYS_PER_WEEK;232 using time_constants::ISO_FIRST_DAY_OF_YEAR;233 using time_constants::MONDAY;234 using time_constants::WeekDay;235 using time_constants::WEEKS_PER_YEAR;236 237 constexpr WeekDay START_DAY = MONDAY;238 239 // The most recent start_day. The rest of the days into the current week240 // don't count, so ignore them.241 // Also add 7 to handle start_day > tm_wday242 const int start_of_cur_week =243 timeptr->tm_yday -244 ((timeptr->tm_wday + DAYS_PER_WEEK - START_DAY) % DAYS_PER_WEEK);245 246 // if the week starts in the previous year, and also if the 4th of this year247 // is not in this week.248 if (start_of_cur_week < -3) {249 const int days_into_prev_year =250 get_days_in_year(get_year() - 1) + start_of_cur_week;251 // Each year has at least 52 weeks, but a year's last week will be 53 if252 // its first week starts in the previous year and its last week ends253 // in the next year. We know get_year() - 1 must extend into get_year(),254 // so here we check if it also extended into get_year() - 2 and add 1 week255 // if it does.256 return WEEKS_PER_YEAR +257 ((days_into_prev_year % DAYS_PER_WEEK) > ISO_FIRST_DAY_OF_YEAR);258 }259 260 // subtract 1 to account for yday being 0 indexed261 const int days_until_end_of_year =262 get_days_in_year(get_year()) - start_of_cur_week - 1;263 264 // if there are less than 3 days from the start of this week to the end of265 // the year, then there must be 4 days in this week in the next year, which266 // means that this week is the first week of that year.267 if (days_until_end_of_year < 3)268 return 1;269 270 // else just calculate the current week like normal.271 const int ceil_weeks_since_start =272 (start_of_cur_week + DAYS_PER_WEEK) / DAYS_PER_WEEK;273 274 // add 1 if this year's first week starts in the previous year.275 const int WEEK_STARTS_IN_PREV_YEAR =276 ((start_of_cur_week + time_constants::DAYS_PER_WEEK) %277 time_constants::DAYS_PER_WEEK) > time_constants::ISO_FIRST_DAY_OF_YEAR;278 return ceil_weeks_since_start + WEEK_STARTS_IN_PREV_YEAR;279 }280 281 LIBC_INLINE constexpr int get_iso_year() const {282 const int BASE_YEAR = get_year();283 // The ISO year is the same as a standard year for all dates after the start284 // of the first week and before the last week. Since the first ISO week of a285 // year starts on the 4th, anything after that is in this year.286 if (timeptr->tm_yday >= time_constants::ISO_FIRST_DAY_OF_YEAR &&287 timeptr->tm_yday < time_constants::DAYS_PER_NON_LEAP_YEAR -288 time_constants::DAYS_PER_WEEK)289 return BASE_YEAR;290 291 const int ISO_WDAY = get_iso_wday();292 // The first week of the ISO year is defined as the week containing the293 // 4th day of January.294 295 // first week296 if (timeptr->tm_yday < time_constants::ISO_FIRST_DAY_OF_YEAR) {297 /*298 If jan 4 is in this week, then we're in BASE_YEAR, else we're in the299 previous year. The formula's been rearranged so here's the derivation:300 301 +--------+-- days until jan 4302 | |303 wday + (4 - yday) < 7304 | |305 +---------------+-- weekday of jan 4306 307 rearranged to get all the constants on one side:308 309 wday - yday < 7 - 4310 */311 const int IS_CUR_YEAR = (ISO_WDAY - timeptr->tm_yday <312 time_constants::DAYS_PER_WEEK -313 time_constants::ISO_FIRST_DAY_OF_YEAR);314 return BASE_YEAR - !IS_CUR_YEAR;315 }316 317 // last week318 const int DAYS_LEFT_IN_YEAR =319 get_days_in_year(get_year()) - timeptr->tm_yday;320 /*321 Similar to above, we're checking if jan 4 (of next year) is in this week. If322 it is, this is in the next year. Note that this also handles the case of323 yday > days in year gracefully.324 325 +------------------+-- days until jan 4 (of next year)326 | |327 wday + (4 + remaining days) < 7328 | |329 +-------------------------+-- weekday of jan 4330 331 rearranging we get:332 333 wday + remaining days < 7 - 4334 */335 const int IS_NEXT_YEAR =336 (ISO_WDAY + DAYS_LEFT_IN_YEAR <337 time_constants::DAYS_PER_WEEK - time_constants::ISO_FIRST_DAY_OF_YEAR);338 return BASE_YEAR + IS_NEXT_YEAR;339 }340 341 LIBC_INLINE time_t get_epoch() const {342 auto seconds = mktime_internal(timeptr);343 return seconds ? *seconds : time_utils::out_of_range();344 }345 346 // returns the timezone offset in microwave time:347 // return (hours * 100) + minutes;348 // This means that a shift of -4:30 is returned as -430, simplifying349 // conversion.350 LIBC_INLINE constexpr int get_timezone_offset() const {351 // TODO: timezone support352 return 0;353 }354};355 356} // namespace time_utils357} // namespace LIBC_NAMESPACE_DECL358 359#endif // LLVM_LIBC_SRC_TIME_TIME_UTILS_H360