[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 <rtenneti@google.com>
This commit is contained in:
1 parent
4c5e15667f
commit
2705f4613a
8 files changed
+390
-193
No files matched your search
@@ -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.
|
||||
@@ -13,6 +13,7 @@ source_tree_layout
|
||||
entrypoints
|
||||
implementing_a_function
|
||||
config_options
|
||||
date_and_time
|
||||
fuzzing
|
||||
header_generation
|
||||
implementation_standard
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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<cpp::string_view, DAYS_PER_WEEK> WEEK_DAY_NAMES = {
|
||||
@@ -102,8 +91,12 @@ constexpr cpp::array<cpp::string_view, MONTHS_PER_YEAR> 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
|
||||
|
||||
+141
-147
@@ -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<time_t> 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<int64_t>(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<int64_t>(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<time_t> 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<int> 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<int64_t>(time_constants::NUMBER_OF_SECONDS_IN_LEAP_YEAR);
|
||||
@@ -128,89 +99,112 @@ ErrorOr<int> 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<int64_t>((static_cast<UInt128>(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<UInt128>(jul) * C2;
|
||||
int64_t yrs = Y_SHIFT - static_cast<int64_t>(num >> 64);
|
||||
uint64_t low = static_cast<uint64_t>(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<uint64_t>((static_cast<UInt128>(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<int64_t>(ypt);
|
||||
int64_t d_val =
|
||||
static_cast<int64_t>((static_cast<UInt128>(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<int>(d_val + 1);
|
||||
const int month = static_cast<int>(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<int>(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<int>(years + 2000 - time_constants::TIME_YEAR_BASE);
|
||||
tm->tm_mon = static_cast<int>(months + 2);
|
||||
tm->tm_mday = static_cast<int>(remainingDays + 1);
|
||||
tm->tm_wday = static_cast<int>(wday);
|
||||
tm->tm_yday = static_cast<int>(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<int>((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<int>(remainingSeconds / time_constants::SECONDS_PER_HOUR);
|
||||
static_cast<int>(remaining_seconds / time_constants::SECONDS_PER_HOUR);
|
||||
tm->tm_min =
|
||||
static_cast<int>(remainingSeconds / time_constants::SECONDS_PER_MIN %
|
||||
static_cast<int>(remaining_seconds / time_constants::SECONDS_PER_MIN %
|
||||
time_constants::SECONDS_PER_MIN);
|
||||
tm->tm_sec =
|
||||
static_cast<int>(remainingSeconds % time_constants::SECONDS_PER_MIN);
|
||||
// TODO(rtenneti): Need to handle timezone and update of tm_isdst.
|
||||
tm->tm_isdst = 0;
|
||||
static_cast<int>(remaining_seconds % time_constants::SECONDS_PER_MIN);
|
||||
tm->tm_isdst = 0; // Daylight saving time flag (not implemented)
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -122,14 +122,17 @@ LIBC_INLINE ErrorOr<tm *> 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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user