Skip to content

Commit cc63954

Browse files
committed
[ruby/date] Optimize Gregorian date conversions with Neri-Schneider algorithm
Replace floating-point arithmetic and iterative loops with pure integer operations for ~40% faster Date operations. Date.ordinal and Date.commercial are ~2x faster due to O(1) first-day-of-year calculation. Reference: https://arxiv.org/abs/2102.06959
1 parent ea393a9 commit cc63954

File tree

1 file changed

+194
-35
lines changed

1 file changed

+194
-35
lines changed

ext/date/date_core.c

Lines changed: 194 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "ruby/util.h"
88
#include <math.h>
99
#include <time.h>
10+
#include <stdint.h> /* For uint64_t in Neri-Schneider algorithm */
1011
#if defined(HAVE_SYS_TIME_H)
1112
#include <sys/time.h>
1213
#endif
@@ -452,11 +453,27 @@ do {\
452453
static int c_valid_civil_p(int, int, int, double,
453454
int *, int *, int *, int *);
454455

456+
/* Forward declarations for Neri-Schneider optimized functions */
457+
static int c_gregorian_civil_to_jd(int y, int m, int d);
458+
static void c_gregorian_jd_to_civil(int jd, int *ry, int *rm, int *rd);
459+
inline static int c_gregorian_fdoy(int y);
460+
inline static int c_gregorian_ldoy(int y);
461+
inline static int c_gregorian_ldom_jd(int y, int m);
462+
inline static int ns_jd_in_range(int jd);
463+
455464
static int
456465
c_find_fdoy(int y, double sg, int *rjd, int *ns)
457466
{
458467
int d, rm, rd;
459468

469+
/* Fast path: pure Gregorian calendar */
470+
if (isinf(sg) && sg < 0) {
471+
*rjd = c_gregorian_fdoy(y);
472+
*ns = 1;
473+
return 1;
474+
}
475+
476+
/* Keep existing loop for Julian/reform period */
460477
for (d = 1; d < 31; d++)
461478
if (c_valid_civil_p(y, 1, d, sg, &rm, &rd, rjd, ns))
462479
return 1;
@@ -468,6 +485,14 @@ c_find_ldoy(int y, double sg, int *rjd, int *ns)
468485
{
469486
int i, rm, rd;
470487

488+
/* Fast path: pure Gregorian calendar */
489+
if (isinf(sg) && sg < 0) {
490+
*rjd = c_gregorian_ldoy(y);
491+
*ns = 1;
492+
return 1;
493+
}
494+
495+
/* Keep existing loop for Julian/reform period */
471496
for (i = 0; i < 30; i++)
472497
if (c_valid_civil_p(y, 12, 31 - i, sg, &rm, &rd, rjd, ns))
473498
return 1;
@@ -493,6 +518,14 @@ c_find_ldom(int y, int m, double sg, int *rjd, int *ns)
493518
{
494519
int i, rm, rd;
495520

521+
/* Fast path: pure Gregorian calendar */
522+
if (isinf(sg) && sg < 0) {
523+
*rjd = c_gregorian_ldom_jd(y, m);
524+
*ns = 1;
525+
return 1;
526+
}
527+
528+
/* Keep existing loop for Julian/reform period */
496529
for (i = 0; i < 30; i++)
497530
if (c_valid_civil_p(y, m, 31 - i, sg, &rm, &rd, rjd, ns))
498531
return 1;
@@ -502,55 +535,74 @@ c_find_ldom(int y, int m, double sg, int *rjd, int *ns)
502535
static void
503536
c_civil_to_jd(int y, int m, int d, double sg, int *rjd, int *ns)
504537
{
505-
double a, b, jd;
538+
int jd;
506539

507-
if (m <= 2) {
508-
y -= 1;
509-
m += 12;
540+
/* Fast path: pure Gregorian calendar (sg == -infinity) */
541+
if (isinf(sg) && sg < 0) {
542+
*rjd = c_gregorian_civil_to_jd(y, m, d);
543+
*ns = 1;
544+
return;
510545
}
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;
546+
547+
/* Calculate Gregorian JD using optimized algorithm */
548+
jd = c_gregorian_civil_to_jd(y, m, d);
549+
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 (((isinf(sg) && sg < 0) || 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,113 @@ 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_GREGORIAN_EPOCH 1721120
793+
794+
/*
795+
* Safe bounds for Neri-Schneider algorithm to avoid integer overflow.
796+
* These correspond to approximately years -1,000,000 to +1,000,000.
797+
*/
798+
#define NS_JD_MIN -364000000
799+
#define NS_JD_MAX 538000000
800+
801+
inline static int
802+
ns_jd_in_range(int jd)
803+
{
804+
return jd >= NS_JD_MIN && jd <= NS_JD_MAX;
805+
}
806+
807+
/* Optimized: Gregorian date -> Julian Day Number */
808+
static int
809+
c_gregorian_civil_to_jd(int y, int m, int d)
810+
{
811+
/* Shift epoch to March 1 of year 0 (Jan/Feb belong to previous year) */
812+
int j = (m < 3) ? 1 : 0;
813+
int y0 = y - j;
814+
int m0 = j ? m + 12 : m;
815+
int d0 = d - 1;
816+
817+
/* Calculate year contribution with leap year correction */
818+
int q1 = DIV(y0, 100);
819+
int yc = DIV(1461 * y0, 4) - q1 + DIV(q1, 4);
820+
821+
/* Calculate month contribution using integer arithmetic */
822+
int mc = (979 * m0 - 2919) / 32;
823+
824+
/* Combine and add epoch offset to get JDN */
825+
return yc + mc + d0 + NS_GREGORIAN_EPOCH;
826+
}
827+
828+
/* Optimized: Julian Day Number -> Gregorian date */
829+
static void
830+
c_gregorian_jd_to_civil(int jd, int *ry, int *rm, int *rd)
831+
{
832+
int r0, n1, q1, r1, n2, q2, r2, n3, q3, r3, y0, j;
833+
uint64_t u2;
834+
835+
/* Convert JDN to rata die (March 1, Year 0 epoch) */
836+
r0 = jd - NS_GREGORIAN_EPOCH;
837+
838+
/* Extract century and day within 400-year cycle */
839+
/* Use Euclidean (floor) division for negative values */
840+
n1 = 4 * r0 + 3;
841+
q1 = DIV(n1, 146097); /* Century */
842+
r1 = MOD(n1, 146097) / 4; /* Day within 400-year cycle */
843+
844+
/* Use 64-bit arithmetic for year calculation within century */
845+
n2 = 4 * r1 + 3;
846+
u2 = (uint64_t)2939745 * (uint64_t)n2;
847+
q2 = (int)(u2 >> 32); /* Year within century */
848+
r2 = (int)((uint32_t)u2 / 2939745 / 4); /* Day of year */
849+
850+
/* Calculate month and day using integer arithmetic */
851+
n3 = 2141 * r2 + 197913;
852+
q3 = n3 >> 16; /* Month (3-14) */
853+
r3 = (n3 & 0xFFFF) / 2141; /* Day of month (0-based) */
854+
855+
/* Combine century and year */
856+
y0 = 100 * q1 + q2;
857+
858+
/* Adjust for January/February (shift from fiscal year) */
859+
j = (r2 >= 306) ? 1 : 0;
860+
861+
*ry = y0 + j;
862+
*rm = j ? q3 - 12 : q3;
863+
*rd = r3 + 1;
864+
}
865+
866+
/* O(1) first day of year for Gregorian calendar */
867+
inline static int
868+
c_gregorian_fdoy(int y)
869+
{
870+
return c_gregorian_civil_to_jd(y, 1, 1);
871+
}
872+
873+
/* O(1) last day of year for Gregorian calendar */
874+
inline static int
875+
c_gregorian_ldoy(int y)
876+
{
877+
return c_gregorian_civil_to_jd(y, 12, 31);
878+
}
879+
880+
/* O(1) last day of month (JDN) for Gregorian calendar */
881+
inline static int
882+
c_gregorian_ldom_jd(int y, int m)
883+
{
884+
return c_gregorian_civil_to_jd(y, m, c_gregorian_last_day_of_month(y, m));
885+
}
886+
728887
static int
729888
c_valid_julian_p(int y, int m, int d, int *rm, int *rd)
730889
{

0 commit comments

Comments
 (0)