Skip to content

Commit 0bec399

Browse files
authored
Merge pull request #152 from mensfeld/neri-schneider-optimization
Optimize Gregorian date conversions with Neri-Schneider algorithm
2 parents ea393a9 + 47778c3 commit 0bec399

File tree

1 file changed

+229
-36
lines changed

1 file changed

+229
-36
lines changed

ext/date/date_core.c

Lines changed: 229 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -452,11 +452,43 @@ do {\
452452
static int c_valid_civil_p(int, int, int, double,
453453
int *, int *, int *, int *);
454454

455+
/* Check if using pure Gregorian calendar (sg == -Infinity) */
456+
#define c_gregorian_only_p(sg) (isinf(sg) && (sg) < 0)
457+
458+
/*
459+
* Fast path macros for pure Gregorian calendar.
460+
* Sets *rjd to the JD value, *ns to 1 (new style), and returns.
461+
*/
462+
#define GREGORIAN_JD_FAST_PATH_RET(sg, jd_expr, rjd, ns) \
463+
if (c_gregorian_only_p(sg)) { \
464+
*(rjd) = (jd_expr); \
465+
*(ns) = 1; \
466+
return 1; \
467+
}
468+
469+
#define GREGORIAN_JD_FAST_PATH(sg, jd_expr, rjd, ns) \
470+
if (c_gregorian_only_p(sg)) { \
471+
*(rjd) = (jd_expr); \
472+
*(ns) = 1; \
473+
return; \
474+
}
475+
476+
/* Forward declarations for Neri-Schneider optimized functions */
477+
static int c_gregorian_civil_to_jd(int y, int m, int d);
478+
static void c_gregorian_jd_to_civil(int jd, int *ry, int *rm, int *rd);
479+
static int c_gregorian_fdoy(int y);
480+
static int c_gregorian_ldoy(int y);
481+
static int c_gregorian_ldom_jd(int y, int m);
482+
static int ns_jd_in_range(int jd);
483+
455484
static int
456485
c_find_fdoy(int y, double sg, int *rjd, int *ns)
457486
{
458487
int d, rm, rd;
459488

489+
GREGORIAN_JD_FAST_PATH_RET(sg, c_gregorian_fdoy(y), rjd, ns);
490+
491+
/* Keep existing loop for Julian/reform period */
460492
for (d = 1; d < 31; d++)
461493
if (c_valid_civil_p(y, 1, d, sg, &rm, &rd, rjd, ns))
462494
return 1;
@@ -468,6 +500,9 @@ c_find_ldoy(int y, double sg, int *rjd, int *ns)
468500
{
469501
int i, rm, rd;
470502

503+
GREGORIAN_JD_FAST_PATH_RET(sg, c_gregorian_ldoy(y), rjd, ns);
504+
505+
/* Keep existing loop for Julian/reform period */
471506
for (i = 0; i < 30; i++)
472507
if (c_valid_civil_p(y, 12, 31 - i, sg, &rm, &rd, rjd, ns))
473508
return 1;
@@ -493,6 +528,9 @@ c_find_ldom(int y, int m, double sg, int *rjd, int *ns)
493528
{
494529
int i, rm, rd;
495530

531+
GREGORIAN_JD_FAST_PATH_RET(sg, c_gregorian_ldom_jd(y, m), rjd, ns);
532+
533+
/* Keep existing loop for Julian/reform period */
496534
for (i = 0; i < 30; i++)
497535
if (c_valid_civil_p(y, m, 31 - i, sg, &rm, &rd, rjd, ns))
498536
return 1;
@@ -502,55 +540,69 @@ c_find_ldom(int y, int m, double sg, int *rjd, int *ns)
502540
static void
503541
c_civil_to_jd(int y, int m, int d, double sg, int *rjd, int *ns)
504542
{
505-
double a, b, jd;
543+
int jd;
544+
545+
GREGORIAN_JD_FAST_PATH(sg, c_gregorian_civil_to_jd(y, m, d), rjd, ns);
546+
547+
/* Calculate Gregorian JD using optimized algorithm */
548+
jd = c_gregorian_civil_to_jd(y, m, d);
506549

507-
if (m <= 2) {
508-
y -= 1;
509-
m += 12;
510-
}
511-
a = floor(y / 100.0);
512-
b = 2 - a + floor(a / 4.0);
513-
jd = floor(365.25 * (y + 4716)) +
514-
floor(30.6001 * (m + 1)) +
515-
d + b - 1524;
516550
if (jd < sg) {
517-
jd -= b;
551+
/* Before Gregorian switchover - use Julian calendar */
552+
int y2 = y, m2 = m;
553+
if (m2 <= 2) {
554+
y2 -= 1;
555+
m2 += 12;
556+
}
557+
jd = (int)(floor(365.25 * (y2 + 4716)) +
558+
floor(30.6001 * (m2 + 1)) +
559+
d - 1524);
518560
*ns = 0;
519561
}
520-
else
562+
else {
521563
*ns = 1;
564+
}
522565

523-
*rjd = (int)jd;
566+
*rjd = jd;
524567
}
525568

526569
static void
527570
c_jd_to_civil(int jd, double sg, int *ry, int *rm, int *rdom)
528571
{
529-
double x, a, b, c, d, e, y, m, dom;
530-
531-
if (jd < sg)
532-
a = jd;
533-
else {
534-
x = floor((jd - 1867216.25) / 36524.25);
535-
a = jd + 1 + x - floor(x / 4.0);
536-
}
537-
b = a + 1524;
538-
c = floor((b - 122.1) / 365.25);
539-
d = floor(365.25 * c);
540-
e = floor((b - d) / 30.6001);
541-
dom = b - d - floor(30.6001 * e);
542-
if (e <= 13) {
543-
m = e - 1;
544-
y = c - 4716;
545-
}
546-
else {
547-
m = e - 13;
548-
y = c - 4715;
572+
/* Fast path: pure Gregorian or date after switchover, within safe range */
573+
if ((c_gregorian_only_p(sg) || jd >= sg) && ns_jd_in_range(jd)) {
574+
c_gregorian_jd_to_civil(jd, ry, rm, rdom);
575+
return;
549576
}
550577

551-
*ry = (int)y;
552-
*rm = (int)m;
553-
*rdom = (int)dom;
578+
/* Original algorithm for Julian calendar or extreme dates */
579+
{
580+
double x, a, b, c, d, e, y, m, dom;
581+
582+
if (jd < sg)
583+
a = jd;
584+
else {
585+
x = floor((jd - 1867216.25) / 36524.25);
586+
a = jd + 1 + x - floor(x / 4.0);
587+
}
588+
b = a + 1524;
589+
c = floor((b - 122.1) / 365.25);
590+
d = floor(365.25 * c);
591+
e = floor((b - d) / 30.6001);
592+
dom = b - d - floor(30.6001 * e);
593+
if (e <= 13) {
594+
m = e - 1;
595+
y = c - 4716;
596+
}
597+
else {
598+
m = e - 13;
599+
y = c - 4715;
600+
}
601+
602+
*ry = (int)y;
603+
*rm = (int)m;
604+
*rdom = (int)dom;
605+
}
554606
}
555607

556608
static void
@@ -725,6 +777,147 @@ c_gregorian_last_day_of_month(int y, int m)
725777
return monthtab[c_gregorian_leap_p(y) ? 1 : 0][m];
726778
}
727779

780+
/*
781+
* Neri-Schneider algorithm for optimized Gregorian date conversion.
782+
* Reference: Neri & Schneider, "Euclidean Affine Functions and Applications
783+
* to Calendar Algorithms", Software: Practice and Experience, 2023.
784+
* https://arxiv.org/abs/2102.06959
785+
*
786+
* This algorithm provides ~2-3x speedup over traditional floating-point
787+
* implementations by using pure integer arithmetic with multiplication
788+
* and bit-shifts instead of expensive division operations.
789+
*/
790+
791+
/* JDN of March 1, Year 0 in proleptic Gregorian calendar */
792+
#define NS_EPOCH 1721120
793+
794+
/* Days in a 4-year cycle (3 normal years + 1 leap year) */
795+
#define NS_DAYS_IN_4_YEARS 1461
796+
797+
/* Days in a 400-year Gregorian cycle (97 leap years in 400 years) */
798+
#define NS_DAYS_IN_400_YEARS 146097
799+
800+
/* Years per century */
801+
#define NS_YEARS_PER_CENTURY 100
802+
803+
/*
804+
* Multiplier for extracting year within century using fixed-point arithmetic.
805+
* This is ceil(2^32 / NS_DAYS_IN_4_YEARS) for the Euclidean affine function.
806+
*/
807+
#define NS_YEAR_MULTIPLIER 2939745
808+
809+
/*
810+
* Coefficients for month calculation from day-of-year.
811+
* Maps day-of-year to month using: month = (NS_MONTH_COEFF * doy + NS_MONTH_OFFSET) >> 16
812+
*/
813+
#define NS_MONTH_COEFF 2141
814+
#define NS_MONTH_OFFSET 197913
815+
816+
/*
817+
* Coefficients for civil date to JDN month contribution.
818+
* Maps month to accumulated days: days = (NS_CIVIL_MONTH_COEFF * m - NS_CIVIL_MONTH_OFFSET) / 32
819+
*/
820+
#define NS_CIVIL_MONTH_COEFF 979
821+
#define NS_CIVIL_MONTH_OFFSET 2919
822+
#define NS_CIVIL_MONTH_DIVISOR 32
823+
824+
/* Days from March 1 to December 31 (for Jan/Feb year adjustment) */
825+
#define NS_DAYS_BEFORE_NEW_YEAR 306
826+
827+
/*
828+
* Safe bounds for Neri-Schneider algorithm to avoid integer overflow.
829+
* These correspond to approximately years -1,000,000 to +1,000,000.
830+
*/
831+
#define NS_JD_MIN -364000000
832+
#define NS_JD_MAX 538000000
833+
834+
inline static int
835+
ns_jd_in_range(int jd)
836+
{
837+
return jd >= NS_JD_MIN && jd <= NS_JD_MAX;
838+
}
839+
840+
/* Optimized: Gregorian date -> Julian Day Number */
841+
static int
842+
c_gregorian_civil_to_jd(int y, int m, int d)
843+
{
844+
/* Shift epoch to March 1 of year 0 (Jan/Feb belong to previous year) */
845+
int j = (m < 3) ? 1 : 0;
846+
int y0 = y - j;
847+
int m0 = j ? m + 12 : m;
848+
int d0 = d - 1;
849+
850+
/* Calculate year contribution with leap year correction */
851+
int q1 = DIV(y0, NS_YEARS_PER_CENTURY);
852+
int yc = DIV(NS_DAYS_IN_4_YEARS * y0, 4) - q1 + DIV(q1, 4);
853+
854+
/* Calculate month contribution using integer arithmetic */
855+
int mc = (NS_CIVIL_MONTH_COEFF * m0 - NS_CIVIL_MONTH_OFFSET) / NS_CIVIL_MONTH_DIVISOR;
856+
857+
/* Combine and add epoch offset to get JDN */
858+
return yc + mc + d0 + NS_EPOCH;
859+
}
860+
861+
/* Optimized: Julian Day Number -> Gregorian date */
862+
static void
863+
c_gregorian_jd_to_civil(int jd, int *ry, int *rm, int *rd)
864+
{
865+
int r0, n1, q1, r1, n2, q2, r2, n3, q3, r3, y0, j;
866+
uint64_t u2;
867+
868+
/* Convert JDN to rata die (March 1, Year 0 epoch) */
869+
r0 = jd - NS_EPOCH;
870+
871+
/* Extract century and day within 400-year cycle */
872+
/* Use Euclidean (floor) division for negative values */
873+
n1 = 4 * r0 + 3;
874+
q1 = DIV(n1, NS_DAYS_IN_400_YEARS);
875+
r1 = MOD(n1, NS_DAYS_IN_400_YEARS) / 4;
876+
877+
/* Calculate year within century and day of year */
878+
n2 = 4 * r1 + 3;
879+
/* Use 64-bit arithmetic to avoid overflow */
880+
u2 = (uint64_t)NS_YEAR_MULTIPLIER * (uint64_t)n2;
881+
q2 = (int)(u2 >> 32);
882+
r2 = (int)((uint32_t)u2 / NS_YEAR_MULTIPLIER / 4);
883+
884+
/* Calculate month and day using integer arithmetic */
885+
n3 = NS_MONTH_COEFF * r2 + NS_MONTH_OFFSET;
886+
q3 = n3 >> 16;
887+
r3 = (n3 & 0xFFFF) / NS_MONTH_COEFF;
888+
889+
/* Combine century and year */
890+
y0 = NS_YEARS_PER_CENTURY * q1 + q2;
891+
892+
/* Adjust for January/February (shift from fiscal year) */
893+
j = (r2 >= NS_DAYS_BEFORE_NEW_YEAR) ? 1 : 0;
894+
895+
*ry = y0 + j;
896+
*rm = j ? q3 - 12 : q3;
897+
*rd = r3 + 1;
898+
}
899+
900+
/* O(1) first day of year for Gregorian calendar */
901+
inline static int
902+
c_gregorian_fdoy(int y)
903+
{
904+
return c_gregorian_civil_to_jd(y, 1, 1);
905+
}
906+
907+
/* O(1) last day of year for Gregorian calendar */
908+
inline static int
909+
c_gregorian_ldoy(int y)
910+
{
911+
return c_gregorian_civil_to_jd(y, 12, 31);
912+
}
913+
914+
/* O(1) last day of month (JDN) for Gregorian calendar */
915+
inline static int
916+
c_gregorian_ldom_jd(int y, int m)
917+
{
918+
return c_gregorian_civil_to_jd(y, m, c_gregorian_last_day_of_month(y, m));
919+
}
920+
728921
static int
729922
c_valid_julian_p(int y, int m, int d, int *rm, int *rd)
730923
{

0 commit comments

Comments
 (0)