A Faster days_from_civil Using Magic-Number Division
Howard Hinnant’s days_from_civil is a well-known algorithm for converting a civil year, month, and day into a day count relative to the Unix epoch.
This article restricts the input years to 1 through 9999 and replaces the /400, /100, /4, and /5 operations in days_from_civil with two independent 32-bit integer multiplications, shifts, and a 12-element lookup table.
In inline batch benchmarks on Apple Silicon, the bounded implementation reduced execution time by approximately 37–80% compared with the usual Hinnant implementation across Bun, Rust, and C in the tested environment.
We will start with the complete implementations, then explain why 1461 combines the common-year and four-year terms, why 5243 implements both /100 and /400, and how the epoch offset is folded into the month table.
Implementations
The preconditions are:
year:1..9999month:0..11, where 0 is Januaryday:1..31
These functions do not validate whether the date actually exists. For example, February 31 is accepted as an arithmetic input.
TypeScript
const EPOCH_MONTH_TABLE = new Int32Array([
-719_163, -719_132, -719_469, -719_438,
-719_408, -719_377, -719_347, -719_316,
-719_285, -719_255, -719_224, -719_194,
]);
/** Returns days since 1970-01-01. */
export function ymdToEpochDays(
year: number,
month: number,
day: number,
): number {
const adjustedYear = year - (month <= 1 ? 1 : 0);
const centuryProduct = Math.imul(adjustedYear, 5_243);
return (
(Math.imul(adjustedYear, 1_461) >>> 2) -
(centuryProduct >>> 19) +
(centuryProduct >>> 21) +
EPOCH_MONTH_TABLE[month] +
day
);
}
TypeScript uses Math.imul to request 32-bit integer multiplication explicitly. All shifted values are nonnegative under the preconditions, so unsigned right shift, >>>, is used.
Rust
const EPOCH_MONTH_TABLE: [i32; 12] = [
-719_163, -719_132, -719_469, -719_438,
-719_408, -719_377, -719_347, -719_316,
-719_285, -719_255, -719_224, -719_194,
];
/// Returns days since 1970-01-01.
///
/// Preconditions:
/// - year: 1..=9999
/// - month: 0..=11
/// - day: 1..=31
#[inline]
pub fn ymd_to_epoch_days(year: u32, month: u32, day: u32) -> i32 {
let adjusted_year = year - u32::from(month <= 1);
let century_product = adjusted_year * 5_243;
((adjusted_year * 1_461) >> 2) as i32
- (century_product >> 19) as i32
+ (century_product >> 21) as i32
+ EPOCH_MONTH_TABLE[month as usize]
+ day as i32
}
C
#include <stdint.h>
/*
* Returns days since 1970-01-01.
*
* Preconditions:
* - year: 1..9999
* - month: 0..11
* - day: 1..31
*/
static inline int32_t ymd_to_epoch_days(
uint32_t year,
uint32_t month,
uint32_t day
) {
static const int32_t epoch_month_table[12] = {
-719163, -719132, -719469, -719438,
-719408, -719377, -719347, -719316,
-719285, -719255, -719224, -719194
};
const uint32_t adjusted_year = year - (month <= 1u);
const uint32_t century_product = adjusted_year * 5243u;
return (int32_t)((adjusted_year * 1461u) >> 2)
- (int32_t)(century_product >> 19)
+ (int32_t)(century_product >> 21)
+ epoch_month_table[month]
+ (int32_t)day;
}
The Usual days_from_civil
Howard Hinnant’s implementation, adapted to zero-based months, can be written as follows:
function daysFromCivil(year: number, month: number, day: number): number {
year -= month <= 1 ? 1 : 0;
const era = Math.floor(year / 400);
const yearOfEra = year - era * 400;
const marchMonth = month + (month > 1 ? -2 : 10);
const dayOfYear =
Math.floor((153 * marchMonth + 2) / 5) + day - 1;
const dayOfEra =
yearOfEra * 365 +
Math.floor(yearOfEra / 4) -
Math.floor(yearOfEra / 100) +
dayOfYear;
return era * 146_097 + dayOfEra - 719_468;
}
January and February are treated as the final two months of the preceding computational year. This moves the leap day to the end of that year. The calculation then splits the date into a 400-year era, a year within the era, and a day counted from March 1.
The Transformations
There are three main transformations.
First, the common-year contribution and the /4 leap-year contribution are combined:
Math.imul(adjustedYear, 1_461) >>> 2
Second, /100 and /400 are obtained from one shared product:
const product = Math.imul(adjustedYear, 5_243);
product >>> 19; // adjustedYear / 100
product >>> 21; // adjustedYear / 400
Third, the month contribution, -1, and the Unix epoch offset are folded into one lookup table.
The number of days before each month has only 12 possible values, so (153 * month + 2) / 5 is replaced with a lookup table.
const EPOCH_MONTH_TABLE = new Int32Array([
-719_163, -719_132, -719_469, -719_438,
-719_408, -719_377, -719_347, -719_316,
-719_285, -719_255, -719_224, -719_194,
]);
Combining the 365-Day and Four-Year Terms
The year contribution begins with:
365 * adjustedYear + floor(adjustedYear / 4)
Since 1461 = 4 * 365 + 1:
floor(1461 * adjustedYear / 4)
= 365 * adjustedYear + floor(adjustedYear / 4)
The input is nonnegative, so the division by four becomes an unsigned shift:
Math.imul(adjustedYear, 1_461) >>> 2
This replaces a multiplication, an independent shift, and their addition with one multiplication followed by a shift.
Folding the Epoch Offset into the Month Table
The original tail of the expression is:
DOY_TABLE[month] + day - 1 - 719468
The constant terms can be precomputed for each month:
EPOCH_MONTH_TABLE[month]
= DOY_TABLE[month] - 719469
The runtime expression then becomes only:
EPOCH_MONTH_TABLE[month] + day
For example, January uses 306 - 719469 = -719163. The table remains 48 bytes; only its values change.
Why 5243 Implements Division by 100
After the January/February adjustment, the range is:
0 <= adjustedYear <= 9999
Write the adjusted year as:
adjustedYear = 100q + r
0 <= r < 100
We have:
5243 * 100
= 524300
= 2^19 + 12
Therefore:
5243 * adjustedYear
= 2^19 * q + 12q + 5243r
Within the input range, q <= 99 and r <= 99. The maximum value of the remaining part is:
12 * 99 + 5243 * 99
= 520245
< 2^19
It cannot carry into bit 19. Consequently:
Math.imul(adjustedYear, 5243) >>> 19
is exactly floor(adjustedYear / 100) throughout the bounded range.
Why the Same Product Implements Division by 400
Now write:
adjustedYear = 400q + r
0 <= r < 400
We also have:
5243 * 400
= 2097200
= 2^21 + 48
Thus:
5243 * adjustedYear
= 2^21 * q + 48q + 5243r
Here q <= 24 and r <= 399. The maximum remaining part is:
48 * 24 + 5243 * 399
= 2093109
< 2^21
It cannot carry into bit 21. Therefore:
Math.imul(adjustedYear, 5243) >>> 21
is exactly floor(adjustedYear / 400) throughout the range.
One multiplication can consequently be shared:
const product = Math.imul(adjustedYear, 5243);
const century = product >>> 19;
const era = product >>> 21;
Replacing the Month Division with a Table
The March-based calendar uses:
floor((153 * marchMonth + 2) / 5)
marchMonth has only 12 possible values. Expanded and reordered for zero-based Gregorian months, those values are:
Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
306 337 0 31 61 92 122 153 184 214 245 275
The lookup replaces the month adjustment branch, multiplication, addition, and /5. The final implementation additionally stores each value after subtracting 719469, folding both the Unix epoch offset and the day-index correction into the same table.
The table occupies 48 bytes. That is small on ordinary PCs and servers, but the relative performance of a table and arithmetic can still depend on cold-cache behavior, code size, bounds checks, and JIT optimization.
32-Bit Integer Bounds
Using the inclusive upper bound of 9999, the largest products are:
9999 * 5243
= 52424757
< 2^31
9999 * 1461
= 14608539
< 2^31
The TypeScript implementation therefore does not wrap a signed 32-bit Math.imul result.
All intermediate values fit comfortably in 32 bits. Across the complete specified input set, the output range was:
-719162 <= result <= 2932896
Exhaustive Verification
Every input in the bounded domain was compared with the usual Hinnant implementation:
for (let year = 1; year <= 9_999; year++) {
for (let month = 0; month < 12; month++) {
for (let day = 1; day <= 31; day++) {
const expected = daysFromCivil(year, month, day);
const actual = ymdToEpochDays(year, month, day);
if (actual !== expected) {
throw new Error(
`mismatch: ${year}-${month}-${day}: ` +
`${actual} !== ${expected}`,
);
}
}
}
}
The number of comparisons was:
9999 * 12 * 31
= 3,719,628
The TypeScript, Rust, and C implementations all matched across the complete set.
The test intentionally checks every day through 31 without accounting for the actual length of each month. It therefore includes nonexistent dates such as February 31. The conversion remains arithmetically consistent, but callers are responsible for validating real civil dates.
Benchmarks
The benchmarks were run on Apple Silicon ARM64. Each implementation processed a pre-generated array of 2^20 dates 32 times. The input position was rotated between rounds to prevent an optimizer from hoisting an identical loop.
Inputs were generated with a deterministic pseudorandom sequence over:
- year:
1..9999 - month:
0..11 - day:
1..28
The best result from seven trials was recorded. The toolchain was:
Bun 1.4.0
rustc 1.96.0 -C opt-level=3 -C target-cpu=native
Apple clang 21.0.0 -O3 -march=native
Results:
| Environment | Usual Hinnant | Bounded version | Time reduction | Throughput |
|---|---|---|---|---|
| Bun | 14.738 ns/item | 3.018 ns/item | 79.5% | 4.88x |
| Rust | 2.001 ns/item | 1.192 ns/item | 40.4% | 1.68x |
| C | 1.923 ns/item | 1.205 ns/item | 37.3% | 1.60x |
The effect is especially large in Bun, where ordinary floating-point division plus Math.floor is replaced with Math.imul and shifts.
C and Rust compilers can already lower division by constants to multiplication and shifts. Even so, combining the 365-day and /4 terms, sharing one product between /100 and /400, and folding the remaining constants into the month table reduced execution time by about 37–40% in this environment.
These results apply to one CPU, compiler/JIT version, and input distribution. Intel x86-64, other Apple Silicon generations, Node.js/V8, browsers, and different Rust or Clang versions may produce different results.
Prior Work and Positioning
This implementation is based on Howard Hinnant’s days_from_civil. Strength reduction from division by constants to multiplication and shifts is also well established.
Division by 100 using 5243 over integers from 0 through 9999 is itself known:
(value * 5243) >> 19
The construction here applies the bounded shared-product technique from the earlier article, “A Faster Day-of-Week Calculation Using Magic Numbers,” to days_from_civil.
The relevant combination is:
- obtain
/100and/400from the sameadjustedYear * 5243product; - combine
365 * adjustedYear + floor(adjustedYear / 4)asfloor(1461 * adjustedYear / 4); - replace the March-based month expression with a 12-element table that also contains the epoch offset;
- use explicit
Math.imuland unsigned shifts in JavaScript; and - complete the calculation in 32 bits by restricting years to
1..9999.
Neri and Schneider systematically study multiplication-and-shift Gregorian calendar conversion and dependency-chain reduction through Euclidean affine functions. The individual ingredients and the general optimization strategy are therefore not claimed as new.
Within the public implementations examined, however, I did not find the same bounded days_from_civil construction combining the 1461 year product, one 5243 product for both /100 and /400, and this epoch-adjusted month table.
This does not establish optimality across all expressions, languages, CPUs, or input domains. It is a target-specific optimization for a clearly bounded year range.
The Inverse Conversion
This article covers days_from_civil, which converts a year, month, and day into a serial day number. The inverse operation is normally called civil_from_days:
civil date -- days_from_civil --> serial day
civil date <-- civil_from_days --- serial day
Although the two functions are inverses, their optimization structures differ substantially. civil_from_days must recover the era, year, month, and day from one integer, and it uses different divisions, corrections, and magic constants. It is therefore best treated separately.
Summary
Restricting the year to 1..9999 allows the constant divisions in Hinnant-style days_from_civil to be replaced with two independent multiplications and shifts.
The central transformations are:
Math.imul(adjustedYear, 1_461) >>> 2;
const centuryProduct = Math.imul(adjustedYear, 5_243);
centuryProduct >>> 19; // adjustedYear / 100
centuryProduct >>> 21; // adjustedYear / 400
and:
EPOCH_MONTH_TABLE[month] + day;
In the Apple Silicon benchmarks reported here, the bounded version reduced execution time by approximately 80% in Bun and 37–40% in Rust and C compared with the usual Hinnant implementation.
For ordinary application code, the clearer and broadly ranged days_from_civil is generally sufficient. This bounded implementation is intended for cases where civil-date conversion is on a hot path, the input range is guaranteed, and the performance difference has been measured on the target environment.
References
- Howard Hinnant, chrono-Compatible Low-Level Date Algorithms
- Cassio Neri and Lorenz Schneider, Euclidean Affine Functions and Applications to Calendar Algorithms
- Cassio Neri, EAF supplementary material
- A Faster Day-of-Week Calculation Using Magic Numbers