Faster Ordinal Date Algorithms

Draft Date: 3 January 2026

This is an early draft blog post. Much more detail and explanation will be added later.
The algorithms are unlikely to change much unless something new comes up.


.......
.......
.......

The below algorithm computes year, ordinal (1-366), and leap (boolean) given rata-die (days since 1 Jan 1970).

Fast Ordinal Date AlgorithmView in C++
  1. const ERAS = 5949
  2. const D_SHIFT = 146097 * ERAS + 719162 + 366
  3. const Y_SHIFT = 400 * ERAS
  4. const CEN_MUL = (4 << 47) / 146097
  5. const JUL_MUL = (4 << 40) / 1461 + 1
  6. const CEN_CUT = (365 << 32) / 36525
  7. day += D_SHIFT                       // Epoch: -XX00-01-01
  8. c_n = (day * CEN_MUL) >> 15          // Divide 36524.25
  9. cen = c_n >> 32                      // Century
  10. cpt = c_n % (1 << 32)                // Century-part
  11. ijy = cen % 4 == 0 || cpt > CEN_CUT  // "Is Julian Year"
  12. jul = day + cen - cen / 4            // Julian map
  13. y_n = (jul * JUL_MUL) >> 8           // Divide 365.25
  14. yrs = y_n >> 32                      // Year
  15. ypt = y_n % (1 << 32)                // Year-part
  16. year = yrs - Y_SHIFT
  17. ordinal = ((ypt * 1461) >> 34) + ijy
  18. leap = yrs % 4 == 0 & ijy

Note that the above algorithm is mostly 32-bit friendly. The calculations of both c_n and y_nare the only lines that don't translate directly on 32-bit computers, each being compiled to a 4-cycle operation due to the bit-shift overlapping two adjacent 32-bit registers. This small speed cost is partially offset bycen and yrs being "free" on 32-bit computers (grabbing the upper 32-bits is just requires accessing the upper register), making the overall penalty 2 × (4 - 2) = 4 cycles.

The section below demonstrates the steps required to perform the bit-shift by 8 for y_n followed by upper and lower 32-bits for yrs and ypt resepectively. Each box represents a single byte (8 bits), from highest byte to lowest.

In 64-bit:   input = (jul * JUL_MUL) =

87654321

↳ yrs:  
0876
– right-shift input by 5 bytes (40 bits)
↳ ypt:  
5432
– right-shift input by 1 byte (8 bits) + truncate to 4 bytes (a free operation)

As seen above, this is two steps overall.
In 32-bit though, there are four steps:

In 32-bit:  hi =

8765
; lo =
4321

↳ yrs:  
0876
– right-shift hi by 1 byte (8 bits)
↳ ypt:   
5000
– left-shift hi by 3 bytes (24 bits)
0432
– right-shift lo by 1 byte (8 bits)
5432
– bitwise combine the above two results (& operator).

So the penalty here is 2 cycles on 32-bit.
The same principles apply for the calculation of cen and cpt, hence why the overall penalty is described as 4-cycles.

Accuracy and Range

The range of the algorithm in 32-bit is suitable for many applications:

Total Days1,739,698,238~1.7 Billion
Total Years4,763,130~4.7 Million
Max Date+2,383,532-12-30     — Rata Die:   +869,848,022
Min Date−2,379,599-01-01     — Rata Die:   −869,850,215

Ordinal to Month + Day

The below function can be used to calculate the day and month given Year/Ordinal/Leap.
It is similar to the algorithm presented in Calendrical Calculations, as well as the previous time-rs algorithm developed by Jacob Pratt. but with the following changes:

  1. Performs a shift after multiplication by STEP instead of prior, which reduces the number of additions, and allows super-scalar processors to multiply in parallel to calculating shift.
  2. Uses the Neri-Schneider technique to use high and low parts of multiplication, which also particularly speeds up performance on modern super-scalar processors.
  3. Uses platform specific scale for micro optimisations (ARM vs x86).
Fast Month & Day from Year/Ordinal/LeapView in C++
  1. #if IS_ARM
  2.     const SCALE = 1
  3. #else
  4.     const SCALE = 2
  5. #endif
  6. const STEP = 1071 * SCALE
  7. const DIVISOR = SCALE << 15
  8. const SHIFT_0 = DIVISOR - 439 * SCALE
  9. const SHIFT_1 = SHIFT_0 + STEP
  10. const SHIFT_2 = SHIFT_1 + STEP
  11. shift = ordinal < 59 + leap ? SHIFT_0 : (leap ? SHIFT_1 : SHIFT_2)
  12. num = ordinal * STEP + shift
  13. month = num / DIVISOR
  14. day = (num % DIVISOR) / STEP + 1

The modulo by DIVISOR is highlighted in green as it is a free operation on x86 processors. On ARM, this benefit does not exist, however we can get an alternative benefit by using the smaller SCALE, which makes sure that the constants all fit under 16-bits in size, and will thus load faster than otherwise.

This DRAFT article will be improved with more content soon before "official" publication.