From 2705f4613aee08fcbe7292ae4a2ce577c8192d48 Mon Sep 17 00:00:00 2001 From: Jeff Bailey Date: Fri, 10 Jul 2026 21:04:22 +0100 Subject: [PATCH] [libc] Implement fast date conversion algorithm of Ben Joffe (#208312) Replaced the loop-based year/month/day extraction in update_from_seconds with Ben Joffe's "Very Fast 64-bit" date algorithm using multiply-shift division (4 multiplications, 0 hardware divisions). Also replaced the mktime inverse with Ben Joffe's inverse date algorithm. Updated is_leap_year to use the Drepper-Neri-Schneider algorithm (% 25 instead of % 100). Replaced the O(n) yday month-length loop with a constant-time cumulative-days table lookup. Uses UInt128 for portable 128-bit support across 64-bit (native) and 32-bit (software fallback) targets. Raman's benchmarks on x86-64 show a ~13% wall-time improvement: Before: real 2.215s, user 2.214s After: real 1.919s, user 1.913s Algorithm references: https://www.benjoffe.com/fast-date-64 https://www.benjoffe.com/fast-date#inverse https://www.benjoffe.com/fast-leap-year#drepper-neri-schneider --------- Co-authored-by: Raman Tenneti --- libc/docs/dev/date_and_time.md | 52 ++++++ libc/docs/dev/index.md | 1 + libc/src/time/CMakeLists.txt | 19 +- libc/src/time/time_constants.h | 19 +- libc/src/time/time_utils.cpp | 288 ++++++++++++++--------------- libc/src/time/time_utils.h | 15 +- libc/test/src/time/CMakeLists.txt | 39 ++-- libc/test/src/time/gmtime_test.cpp | 150 ++++++++++++++- 8 files changed, 390 insertions(+), 193 deletions(-) create mode 100644 libc/docs/dev/date_and_time.md diff --git a/libc/docs/dev/date_and_time.md b/libc/docs/dev/date_and_time.md new file mode 100644 index 000000000000..9d960a66981f --- /dev/null +++ b/libc/docs/dev/date_and_time.md @@ -0,0 +1,52 @@ +(date_and_time)= + +# Date and Time + +LLVM-libc implements the C and POSIX date and time functions (`gmtime`, +`mktime`, etc.) using high-performance algorithms and a 64-bit `time_t`. +This page documents the design decisions and limitations of the implementation. + +## Calendar Model + +LLVM-libc uses the [proleptic Gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar): the modern +Gregorian leap-year rules (divisible by 4, except centuries, except +quad-centuries) are extended to all dates, including those before the +calendar's adoption on October 15, 1582. + +This is the model required by the C and POSIX standards. It means that dates +before 1582 do not correspond to historical Julian calendar dates. For +example, `gmtime` will report February 29 for the year 400 CE, even though +that date was reckoned differently under the Julian calendar in use at the +time. + +## Leap Seconds + +POSIX §4.16 defines each day as exactly 86 400 seconds. LLVM-libc follows +this convention: leap seconds are not represented and `time_t` values map +to UTC times that ignore leap-second insertions. + +## 64-bit `time_t` + +LLVM-libc requires a 64-bit `time_t`. A 32-bit `time_t` is not +supported. This avoids the [Year 2038 problem](https://en.wikipedia.org/wiki/Year_2038_problem) and provides a valid +range of approximately ±2 billion years from the Unix epoch (January 1, 1970). + +## Algorithms + +The date conversion functions use Ben Joffe's "Very Fast 64-bit" date +algorithm, which replaces the traditional loop-based year/month/day extraction +with multiply-shift division (four multiplications, zero hardware divisions). +The inverse (`mktime`) path uses the companion inverse algorithm from the +same article series. Leap-year testing uses the Drepper–Neri–Schneider +algorithm (modulo 25 instead of modulo 100). + +These algorithms are described in: + +* [Very Fast 64-bit Date Algorithm](https://www.benjoffe.com/fast-date-64) +* [Inverse Date Algorithm](https://www.benjoffe.com/fast-date#inverse) +* [Fast Leap-Year Check](https://www.benjoffe.com/fast-leap-year#drepper-neri-schneider) + +128-bit intermediate arithmetic is used for the multiply-shift steps. On +64-bit targets this maps to the native `__uint128_t`; on 32-bit targets +LLVM-libc's software `UInt<128>` implementation is used, preserving +correctness at a modest performance cost. diff --git a/libc/docs/dev/index.md b/libc/docs/dev/index.md index 3a12cf106679..4ce78540c8fa 100644 --- a/libc/docs/dev/index.md +++ b/libc/docs/dev/index.md @@ -13,6 +13,7 @@ source_tree_layout entrypoints implementing_a_function config_options +date_and_time fuzzing header_generation implementation_standard diff --git a/libc/src/time/CMakeLists.txt b/libc/src/time/CMakeLists.txt index f42b9b3723ca..6ff157805c42 100644 --- a/libc/src/time/CMakeLists.txt +++ b/libc/src/time/CMakeLists.txt @@ -21,16 +21,21 @@ add_object_library( HDRS time_utils.h 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 + libc.hdr.stdint_proxy libc.hdr.types.size_t libc.hdr.types.struct_tm - libc.hdr.stdint_proxy + libc.hdr.types.time_t + libc.include.time + libc.src.__support.CPP.limits + libc.src.__support.CPP.optional + libc.src.__support.CPP.string_view + libc.src.__support.common + libc.src.__support.error_or + libc.src.__support.libc_assert + libc.src.__support.libc_errno + libc.src.__support.macros.config + libc.src.__support.uint128 ) add_entrypoint_object( diff --git a/libc/src/time/time_constants.h b/libc/src/time/time_constants.h index 078c427990a6..e554885ad125 100644 --- a/libc/src/time/time_constants.h +++ b/libc/src/time/time_constants.h @@ -74,17 +74,6 @@ constexpr int ISO_FIRST_DAY_OF_YEAR = 3; // the 4th day of the year, 0-indexed. constexpr int ASCTIME_BUFFER_SIZE = 256; constexpr int ASCTIME_MAX_BYTES = 26; -/* 2000-03-01 (mod 400 year, immediately after feb29 */ -constexpr int64_t SECONDS_UNTIL2000_MARCH_FIRST = - (946684800LL + SECONDS_PER_DAY * (31 + 29)); -constexpr int WEEK_DAY_OF2000_MARCH_FIRST = 3; - -constexpr int DAYS_PER400_YEARS = - (DAYS_PER_NON_LEAP_YEAR * 400) + (400 / 4) - 3; -constexpr int DAYS_PER100_YEARS = - (DAYS_PER_NON_LEAP_YEAR * 100) + (100 / 4) - 1; -constexpr int DAYS_PER4_YEARS = (DAYS_PER_NON_LEAP_YEAR * 4) + 1; - constexpr time_t OUT_OF_RANGE_RETURN_VALUE = -1; constexpr cpp::array WEEK_DAY_NAMES = { @@ -102,8 +91,12 @@ constexpr cpp::array MONTH_FULL_NAMES = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; -constexpr int NON_LEAP_YEAR_DAYS_IN_MONTH[] = {31, 28, 31, 30, 31, 30, - 31, 31, 30, 31, 30, 31}; +// Cumulative number of days before each month in a non-leap year. +// 1-indexed: element [1] is January (0 days before it), +// element [2] is February (31 days before it), etc. +// Element [0] is unused padding for direct month-number indexing. +constexpr int CUMULATIVE_DAYS_BEFORE_MONTH[] = { + 0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334}; } // namespace time_constants } // namespace LIBC_NAMESPACE_DECL diff --git a/libc/src/time/time_utils.cpp b/libc/src/time/time_utils.cpp index 945e555e8efa..1ddb6f159f8f 100644 --- a/libc/src/time/time_utils.cpp +++ b/libc/src/time/time_utils.cpp @@ -1,81 +1,70 @@ -//===-- Implementation of mktime function ---------------------------------===// +//===----------------------------------------------------------------------===// // // 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 +/// Implementation of internal time utility functions. +/// +//===----------------------------------------------------------------------===// #include "src/time/time_utils.h" #include "hdr/stdint_proxy.h" #include "src/__support/CPP/limits.h" // INT_MIN, INT_MAX #include "src/__support/common.h" +#include "src/__support/libc_assert.h" #include "src/__support/macros/config.h" +#include "src/__support/uint128.h" #include "src/time/time_constants.h" namespace LIBC_NAMESPACE_DECL { namespace time_utils { -// TODO: clean this up in a followup patch cpp::optional 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 = - static_cast(tm_out->tm_year) + time_constants::TIME_YEAR_BASE; + // POSIX §4.16: each day is accounted for by exactly 86400 seconds. - // 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; + // Normalize year and month from tm_mon. + int64_t total_months = tm_out->tm_mon; + int64_t year = + tm_out->tm_year + static_cast(time_constants::TIME_YEAR_BASE); + int64_t month = total_months % 12; + year += total_months / 12; + if (month < 0) { + month += 12; + year--; } - bool tm_year_is_leap = time_utils::is_leap_year(tm_year_from_base); + month += 1; // 1-12 range + int64_t day = tm_out->tm_mday; - // 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++; + // Inverse date algorithm from Ben Joffe, "The Julian Map" (Nov 2025). + // https://www.benjoffe.com/fast-date#inverse + // + // Calculates the number of days since 1970-01-01 (Unix epoch). + // + // Key constants: + // S = 14700: biases years positive to avoid negative division. + // YEAR_SHIFT = 400*S, RATA_SHIFT = 719468 + 146097*S + 1 + // where 719468 = days from 0000-02-29 to 1970-01-01. + // 979/32: Neri-Schneider EAF approximation for cumulative month days. + // phase (-2919 or 8829): month offset selecting March-start vs + // January-start, depending on whether month <= 2 ("bump"). + constexpr int64_t S = 14700; + constexpr int64_t YEAR_SHIFT = 400 * S; + constexpr int64_t RATA_SHIFT = 719468 + 146097 * S + 1; - // 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); - } - } - } + bool bump = (month <= 2); + int64_t y = year + YEAR_SHIFT - bump; + int64_t cent = y / 100; + int64_t phase = bump ? 8829 : -2919; + + int64_t y_days = y * 365 + (y / 4) - cent + (cent / 4); + int64_t m_days = (979 * month + phase) / 32; + int64_t total_days = y_days + m_days + day - RATA_SHIFT; // TODO: https://github.com/llvm/llvm-project/issues/121962 // Need to handle timezone and update of tm_isdst. @@ -86,38 +75,20 @@ cpp::optional mktime_internal(const tm *tm_out) { return seconds; } -static int64_t computeRemainingYears(int64_t daysPerYears, - int64_t quotientYears, - int64_t *remainingDays) { - int64_t years = *remainingDays / daysPerYears; - if (years == quotientYears) - years--; - *remainingDays -= years * daysPerYears; - return years; -} - -// First, divide "total_seconds" by the number of seconds in a day to get the -// number of days since Jan 1 1970. The remainder will be used to calculate the -// number of Hours, Minutes and Seconds. +// Update the tm structure's year, month, etc. members from seconds. +// total_seconds is the number of seconds since January 1st, 1970. // -// Then, adjust that number of days by a constant to be the number of days -// since Mar 1 2000. Year 2000 is a multiple of 400, the leap year cycle. This -// makes it easier to count how many leap years have passed using division. +// Uses Ben Joffe's "Very Fast 64-Bit" date algorithm (Article 3, Nov 2025). +// https://www.benjoffe.com/fast-date-64 // -// While calculating numbers of years in the days, the following algorithm -// subdivides the days into the number of 400 years, the number of 100 years and -// the number of 4 years. These numbers of cycle years are used in calculating -// leap day. This is similar to the algorithm used in getNumOfLeapYearsBefore() -// and isLeapYear(). Then compute the total number of years in days from these -// subdivided units. +// The Neri-Schneider "EAF" technique is used for month/day determination: +// https://onlinelibrary.wiley.com/doi/full/10.1002/spe.3172 // -// Compute the number of months from the remaining days. Finally, adjust years -// to be 1900 and months to be from January. +// This uses the proleptic Gregorian calendar: Gregorian leap-year rules are +// extended to all dates, including those before the calendar's adoption in +// 1582. ErrorOr update_from_seconds(time_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}; - + // Range check for valid time_t values constexpr time_t time_min = INT_MIN * static_cast(time_constants::NUMBER_OF_SECONDS_IN_LEAP_YEAR); @@ -128,89 +99,112 @@ ErrorOr update_from_seconds(time_t total_seconds, tm *tm) { if (total_seconds < time_min || total_seconds > time_max) return cpp::unexpected(TIME_OVERFLOW); - int64_t seconds = - total_seconds - time_constants::SECONDS_UNTIL2000_MARCH_FIRST; - int64_t days = seconds / time_constants::SECONDS_PER_DAY; - int64_t remainingSeconds = seconds % time_constants::SECONDS_PER_DAY; - if (remainingSeconds < 0) { - remainingSeconds += time_constants::SECONDS_PER_DAY; + // Step 1: Convert seconds to days + remaining seconds + // Handle negative timestamps correctly (before Unix epoch) + int64_t days = total_seconds / time_constants::SECONDS_PER_DAY; + int64_t remaining_seconds = total_seconds % time_constants::SECONDS_PER_DAY; + if (remaining_seconds < 0) { + remaining_seconds += time_constants::SECONDS_PER_DAY; days--; } - int64_t wday = (time_constants::WEEK_DAY_OF2000_MARCH_FIRST + days) % - time_constants::DAYS_PER_WEEK; - if (wday < 0) - wday += time_constants::DAYS_PER_WEEK; + // Save Unix epoch days for wday calculation later + const int64_t unix_days = days; - // Compute the number of 400 year cycles. - int64_t numOfFourHundredYearCycles = days / time_constants::DAYS_PER400_YEARS; - int64_t remainingDays = days % time_constants::DAYS_PER400_YEARS; - if (remainingDays < 0) { - remainingDays += time_constants::DAYS_PER400_YEARS; - numOfFourHundredYearCycles--; - } + // See pseudocode lines 1-29 at https://www.benjoffe.com/fast-date-64 + // + // Key idea: count years backwards from a far-future epoch so that both + // the 4-year and 400-year cycles start with a leap/long period. This + // eliminates the "+3" offset terms from traditional algorithms and + // enables pure multiply-shift division (4 multiplications, 0 hardware + // divisions). - // The remaining number of years after computing the number of - // "four hundred year cycles" will be 4 hundred year cycles or less in 400 - // years. - int64_t numOfHundredYearCycles = computeRemainingYears( - time_constants::DAYS_PER100_YEARS, 4, &remainingDays); + // ERAS: number of 400-year eras to shift into the future; chosen to + // maximize the symmetric range around the Unix epoch in 64-bit. + constexpr int64_t ERAS = 4726498270LL; + // D_SHIFT: reversed day count from the epoch alignment point 0000-02-29. + // 146097 = days per 400-year era; 719469 = days from 0000-02-29 to + // 1970-01-01 (one day earlier than the 719468 used by forward algorithms). + constexpr int64_t D_SHIFT = 146097LL * ERAS - 719469LL; + // Y_SHIFT: converts reversed year count back to a forward year. + constexpr int64_t Y_SHIFT = 400LL * ERAS - 1; + // C1-C3: fixed-point reciprocals for multiply-shift division. + // The >>64 bit-shift is "free" on 64-bit CPUs (just reads the high + // register of the 128-bit multiplication result). + constexpr uint64_t C1 = 505054698555331ULL; // floor(2^64 * 4 / 146097) + constexpr uint64_t C2 = 50504432782230121ULL; // ceil(2^64 * 4 / 1461) + constexpr uint64_t C3 = 8619973866219416ULL; // floor(2^64 / 2140) - // The remaining number of years after computing the number of - // "hundred year cycles" will be 25 four year cycles or less in 100 years. - int64_t numOfFourYearCycles = computeRemainingYears( - time_constants::DAYS_PER4_YEARS, 25, &remainingDays); + // Pseudocode lines 9-11: Adjust for 100/400 leap year rule (Julian Map). + int64_t rev = D_SHIFT - unix_days; + int64_t cen = static_cast((static_cast(rev) * C1) >> 64); + int64_t jul = rev + cen - (cen / 4); - // The remaining number of years after computing the number of - // "four year cycles" will be 4 one year cycles or less in 4 years. - int64_t remainingYears = computeRemainingYears( - time_constants::DAYS_PER_NON_LEAP_YEAR, 4, &remainingDays); + // Pseudocode lines 14-17: Determine year and year-part. + UInt128 num = static_cast(jul) * C2; + int64_t yrs = Y_SHIFT - static_cast(num >> 64); + uint64_t low = static_cast(num); + // 782432 scales the fractional year into a "year-part" (ypt) that + // encodes day-of-year, deliberately skipping the explicit day-of-year + // step from Neri-Schneider and merging it into ypt. + uint64_t ypt = + static_cast((static_cast(low) * 782432ULL) >> 64); - // Calculate number of years from year 2000. - int64_t years = remainingYears + 4 * numOfFourYearCycles + - 100 * numOfHundredYearCycles + - 400LL * numOfFourHundredYearCycles; + // Pseudocode lines 19-20: Detect Jan/Feb and select month offset. + // 126464: ypt threshold for Jan/Feb (lowest values in the reversed, + // March-based computational year). + bool bump = ypt < 126464ULL; + // 191360 and 977792 differ by exactly 12 * 2^16 = 786432, shifting + // the month by 12 without a conditional subtraction. + int64_t shift = bump ? 191360LL : 977792LL; - int leapDay = - !remainingYears && (numOfFourYearCycles || !numOfHundredYearCycles); + // Pseudocode lines 24-25: Year-modulo-bitshift for leap years. + // N packs month (high 16 bits) and day-part (low 16 bits). + // (yrs % 4) * 512 corrects a 1/4-day-per-year drift introduced by + // skipping the explicit day-of-year step. 512 = 2^16 / 32 / 4, i.e. + // one quarter of a "fake 32-day month" in 16-bit space. + int64_t n_val = (yrs % 4) * 512 + shift - static_cast(ypt); + int64_t d_val = + static_cast((static_cast(n_val & 65535) * C3) >> 64); - // We add 31 and 28 for the number of days in January and February, since our - // starting point was March 1st. - int64_t yday = remainingDays + 31 + 28 + leapDay; - if (yday >= time_constants::DAYS_PER_NON_LEAP_YEAR + leapDay) - yday -= time_constants::DAYS_PER_NON_LEAP_YEAR + leapDay; + const int d = static_cast(d_val + 1); + const int month = static_cast(n_val >> 16); + const int64_t year_full = yrs + (bump ? 1 : 0); - int64_t months = 0; - while (daysInMonth[months] <= remainingDays) { - remainingDays -= daysInMonth[months]; - months++; - } - - if (months >= time_constants::MONTHS_PER_YEAR - 2) { - months -= time_constants::MONTHS_PER_YEAR; - years++; - } - - if (years > INT_MAX || years < INT_MIN) + if (year_full > INT_MAX || year_full < INT_MIN) return cpp::unexpected(TIME_OVERFLOW); + const int year = static_cast(year_full); - // All the data (years, month and remaining days) was calculated from - // March, 2000. Thus adjust the data to be from January, 1900. - tm->tm_year = static_cast(years + 2000 - time_constants::TIME_YEAR_BASE); - tm->tm_mon = static_cast(months + 2); - tm->tm_mday = static_cast(remainingDays + 1); - tm->tm_wday = static_cast(wday); - tm->tm_yday = static_cast(yday); + // Step 4: Calculate day of year (yday) in January-based calendar [0, 365] + // Use cumulative-days lookup table for O(1) computation instead of a loop. + LIBC_ASSERT(month >= 1 && month <= 12); + const bool is_leap = time_utils::is_leap_year(year); + int yday = time_constants::CUMULATIVE_DAYS_BEFORE_MONTH[month] + d - 1; + if (is_leap && month > 2) + yday++; + // Step 5: Calculate day of week [0=Sun, 1=Mon, ..., 6=Sat] + // Unix epoch 1970-01-01 was Thursday (4) + int wday = static_cast((unix_days + 4) % 7); + if (wday < 0) + wday += 7; + + // Step 6: Populate tm structure with all calculated values + tm->tm_year = year - time_constants::TIME_YEAR_BASE; // Years since 1900 + tm->tm_mon = month - 1; // Months [0, 11] + tm->tm_mday = d; // Day of month [1, 31] + tm->tm_wday = wday; // Day of week [0, 6] + tm->tm_yday = yday; // Day of year [0, 365] + + // Calculate time components from remaining seconds tm->tm_hour = - static_cast(remainingSeconds / time_constants::SECONDS_PER_HOUR); + static_cast(remaining_seconds / time_constants::SECONDS_PER_HOUR); tm->tm_min = - static_cast(remainingSeconds / time_constants::SECONDS_PER_MIN % + static_cast(remaining_seconds / time_constants::SECONDS_PER_MIN % time_constants::SECONDS_PER_MIN); tm->tm_sec = - static_cast(remainingSeconds % time_constants::SECONDS_PER_MIN); - // TODO(rtenneti): Need to handle timezone and update of tm_isdst. - tm->tm_isdst = 0; + static_cast(remaining_seconds % time_constants::SECONDS_PER_MIN); + tm->tm_isdst = 0; // Daylight saving time flag (not implemented) return 0; } diff --git a/libc/src/time/time_utils.h b/libc/src/time/time_utils.h index acbf5c759c53..b6b61f1be044 100644 --- a/libc/src/time/time_utils.h +++ b/libc/src/time/time_utils.h @@ -122,14 +122,17 @@ LIBC_INLINE ErrorOr localtime(const time_t *t_ptr) { return time_utils::localtime_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. +// 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) { - return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0)); + 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) { diff --git a/libc/test/src/time/CMakeLists.txt b/libc/test/src/time/CMakeLists.txt index c8e113f06d50..999656698077 100644 --- a/libc/test/src/time/CMakeLists.txt +++ b/libc/test/src/time/CMakeLists.txt @@ -1,17 +1,27 @@ add_custom_target(libc_time_unittests) +add_header_library( + time_test_utils + HDRS + TmHelper.h + TmMatcher.h + DEPENDS + libc.hdr.types.struct_tm + libc.src.__support.macros.config + libc.src.time.time_constants + LibcTest +) + add_libc_unittest( asctime_test SUITE libc_time_unittests SRCS asctime_test.cpp - HDRS - TmHelper.h - TmMatcher.h CXX_STANDARD 20 DEPENDS + .time_test_utils libc.hdr.errno_macros libc.src.time.asctime libc.hdr.types.struct_tm @@ -25,12 +35,10 @@ add_libc_unittest( libc_time_unittests SRCS asctime_r_test.cpp - HDRS - TmHelper.h - TmMatcher.h CXX_STANDARD 20 DEPENDS + .time_test_utils libc.hdr.errno_macros libc.src.time.asctime_r libc.hdr.types.struct_tm @@ -44,12 +52,10 @@ add_libc_unittest( libc_time_unittests SRCS ctime_test.cpp - HDRS - TmHelper.h - TmMatcher.h CXX_STANDARD 20 DEPENDS + .time_test_utils libc.include.time libc.hdr.types.time_t libc.src.time.ctime @@ -64,12 +70,10 @@ add_libc_unittest( libc_time_unittests SRCS ctime_r_test.cpp - HDRS - TmHelper.h - TmMatcher.h CXX_STANDARD 20 DEPENDS + .time_test_utils libc.include.time libc.hdr.types.time_t libc.src.time.ctime_r @@ -169,11 +173,11 @@ add_libc_unittest( libc_time_unittests SRCS gmtime_test.cpp - HDRS - TmMatcher.h DEPENDS + .time_test_utils libc.hdr.errno_macros libc.src.time.gmtime + libc.src.time.time_utils libc.src.__support.CPP.limits libc.hdr.types.struct_tm libc.src.time.time_constants @@ -186,9 +190,8 @@ add_libc_unittest( libc_time_unittests SRCS gmtime_r_test.cpp - HDRS - TmMatcher.h DEPENDS + .time_test_utils libc.src.time.gmtime_r libc.hdr.types.struct_tm libc.src.time.time_constants @@ -201,12 +204,10 @@ add_libc_test( libc_time_unittests SRCS mktime_test.cpp - HDRS - TmHelper.h - TmMatcher.h CXX_STANDARD 20 DEPENDS + .time_test_utils libc.src.time.mktime libc.src.__support.CPP.limits libc.hdr.types.struct_tm diff --git a/libc/test/src/time/gmtime_test.cpp b/libc/test/src/time/gmtime_test.cpp index 0a0d6d0ee482..de3f8fe9d883 100644 --- a/libc/test/src/time/gmtime_test.cpp +++ b/libc/test/src/time/gmtime_test.cpp @@ -8,7 +8,6 @@ #include "hdr/errno_macros.h" -#include "hdr/signal_macros.h" #include "hdr/types/struct_tm.h" #include "src/__support/CPP/limits.h" // INT_MAX, INT_MIN #include "src/time/gmtime.h" @@ -20,6 +19,8 @@ using LlvmLibcGmTime = LIBC_NAMESPACE::testing::ErrnoCheckingTest; +static_assert(sizeof(time_t) == 8, "LLVM libc requires a 64-bit time_t."); + TEST_F(LlvmLibcGmTime, OutOfRange) { time_t seconds = 1 + @@ -310,3 +311,150 @@ TEST_F(LlvmLibcGmTime, Max64BitYear) { 0}), *tm_data); } + +TEST_F(LlvmLibcGmTime, LeapYearRules) { + time_t seconds; + struct tm *tm_data; + + // Non-leap year 1900 (divisible by 100 but not 400) - Test March 1 + // Feb 29, 1900 doesn't exist, so we test March 1 + seconds = -2203891200; + tm_data = LIBC_NAMESPACE::gmtime(&seconds); + EXPECT_TM_EQ((tm{0, // sec + 0, // min + 0, // hr + 1, // day + 2, // tm_mon (March) + 1900 - LIBC_NAMESPACE::time_constants::TIME_YEAR_BASE, + 4, // wday (Thursday) + 59, // yday (Jan 31 + Feb 28) + 0}), + *tm_data); + + // Leap year 2000 (divisible by 400) - Feb 29 exists + seconds = 951782400; + tm_data = LIBC_NAMESPACE::gmtime(&seconds); + EXPECT_TM_EQ((tm{0, // sec + 0, // min + 0, // hr + 29, // day + 1, // tm_mon (February) + 2000 - LIBC_NAMESPACE::time_constants::TIME_YEAR_BASE, + 2, // wday (Tuesday) + 59, // yday (Jan 31 + Feb 29 - 1) + 0}), + *tm_data); + + // Leap year 2400 (divisible by 400) - Feb 29 exists + seconds = 13574563200LL; + tm_data = LIBC_NAMESPACE::gmtime(&seconds); + EXPECT_TM_EQ((tm{0, // sec + 0, // min + 0, // hr + 29, // day + 1, // tm_mon (February) + 2400 - LIBC_NAMESPACE::time_constants::TIME_YEAR_BASE, + 2, // wday (Tuesday) + 59, // yday (Jan 31 + Feb 29 - 1) + 0}), + *tm_data); +} + +TEST_F(LlvmLibcGmTime, CenturyBoundaries) { + time_t seconds; + struct tm *tm_data; + + // 1900-01-01 (Monday) + seconds = -2208988800; + tm_data = LIBC_NAMESPACE::gmtime(&seconds); + EXPECT_TM_EQ((tm{0, // sec + 0, // min + 0, // hr + 1, // day + 0, // tm_mon (January) + 1900 - LIBC_NAMESPACE::time_constants::TIME_YEAR_BASE, + 1, // wday (Monday) + 0, // yday + 0}), + *tm_data); + + // 2100-01-01 (Friday) + seconds = 4102444800; + tm_data = LIBC_NAMESPACE::gmtime(&seconds); + EXPECT_TM_EQ((tm{0, // sec + 0, // min + 0, // hr + 1, // day + 0, // tm_mon (January) + 2100 - LIBC_NAMESPACE::time_constants::TIME_YEAR_BASE, + 5, // wday (Friday) + 0, // yday + 0}), + *tm_data); +} + +TEST_F(LlvmLibcGmTime, FarPastAndFuture) { + time_t seconds; + struct tm *tm_data; + + // Far past: year 1000 (Wednesday) + seconds = -30610224000LL; + tm_data = LIBC_NAMESPACE::gmtime(&seconds); + EXPECT_TM_EQ((tm{0, // sec + 0, // min + 0, // hr + 1, // day + 0, // tm_mon (January) + 1000 - LIBC_NAMESPACE::time_constants::TIME_YEAR_BASE, + 3, // wday (Wednesday) + 0, // yday + 0}), + *tm_data); + + // Far future: year 3000 (Wednesday) + seconds = 32503680000LL; + tm_data = LIBC_NAMESPACE::gmtime(&seconds); + EXPECT_TM_EQ((tm{0, // sec + 0, // min + 0, // hr + 1, // day + 0, // tm_mon (January) + 3000 - LIBC_NAMESPACE::time_constants::TIME_YEAR_BASE, + 3, // wday (Wednesday) + 0, // yday + 0}), + *tm_data); +} + +TEST_F(LlvmLibcGmTime, KeyYears) { + // Test Jan 1 of key years to ensure calendar correctness + struct TestCase { + time_t timestamp; + int year; + int wday; + const char *description; + }; + + TestCase cases[] = { + {-2208988800, 1900, 1, "1900-01-01 Monday (non-leap century)"}, + {0, 1970, 4, "1970-01-01 Thursday (epoch)"}, + {915148800, 1999, 5, "1999-01-01 Friday"}, + {946684800, 2000, 6, "2000-01-01 Saturday (leap century)"}, + {978307200, 2001, 1, "2001-01-01 Monday"}, + {4102444800, 2100, 5, "2100-01-01 Friday (non-leap century)"}, + {13569465600LL, 2400, 6, "2400-01-01 Saturday (leap century)"}, + }; + + for (const auto &tc : cases) { + time_t seconds = tc.timestamp; + struct tm *tm_data = LIBC_NAMESPACE::gmtime(&seconds); + ASSERT_NE(tm_data, nullptr) << tc.description; + EXPECT_EQ(tm_data->tm_year, + tc.year - LIBC_NAMESPACE::time_constants::TIME_YEAR_BASE) + << tc.description; + EXPECT_EQ(tm_data->tm_mon, 0) << tc.description; + EXPECT_EQ(tm_data->tm_mday, 1) << tc.description; + EXPECT_EQ(tm_data->tm_wday, tc.wday) << tc.description; + EXPECT_EQ(tm_data->tm_yday, 0) << tc.description; + } +}