Skip to content

Commit f088ecc

Browse files
author
Rick Hull
committed
add pure int/divmod ordinal-to-ymd impl
1 parent c71580f commit f088ecc

File tree

3 files changed

+126
-47
lines changed

3 files changed

+126
-47
lines changed

lib/compsci/date.rb

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def self.year_and_day(day_count)
124124
year = self.guess_year(day_count)
125125
year_days = self.year_days(year - 1)
126126

127-
# rewind the guess as needed
127+
# rewind the guess as needed, typically 0 or 1x, rarely 2x
128128
while year > MIN_Y and year_days >= day_count
129129
year -= 1
130130
year_days -= self.annual_days(year)
@@ -146,12 +146,61 @@ def self.to_ordinal(year, month, day)
146146

147147
# convert days since epoch back to Date
148148
def self.from_ordinal(day_count)
149+
year, month, day = Date.to_ymd_flt(day_count)
150+
Date.new(year:, month:, day:, ordinal_day: day_count)
151+
end
152+
153+
# use floating point and heuristic, very efficient
154+
def self.to_ymd_flt(day_count)
149155
unless day_count > 0
150156
raise(RuntimeError, "day_count should be positive: #{day_count}")
151157
end
152-
year, annual_days = self.year_and_day(day_count)
153-
month, day = self.month_and_day(annual_days, year)
154-
Date.new(year:, month:, day:, ordinal_day: day_count)
158+
year = self.guess_year(day_count)
159+
year_days = self.year_days(year - 1)
160+
161+
# rewind the guess as needed, typically 0 or 1x, rarely 2x
162+
while year > MIN_Y and year_days >= day_count
163+
year -= 1
164+
year_days -= self.annual_days(year)
165+
end
166+
167+
month, day = self.month_and_day(day_count - year_days, year)
168+
[year, month, day]
169+
end
170+
171+
# use pure divmod arithmetic and integers; constant time; ~same~ efficiency
172+
def self.to_ymd_int(day_count)
173+
unless day_count > 0
174+
raise(RuntimeError, "day_count should be positive: #{day_count}")
175+
end
176+
177+
# Convert to 0-based day count for easier math
178+
days = day_count - 1
179+
180+
# 400 year cycle
181+
n400, days = days.divmod(DAYS_400)
182+
183+
# 100-year cycle
184+
n100 = days / DAYS_100
185+
n100 = 3 if n100 == 4
186+
days -= (n100 * DAYS_100)
187+
188+
# 4-year cycle
189+
n4, days = days.divmod(DAYS_4)
190+
191+
# remaining years
192+
n1 = days / ANNUAL_DAYS
193+
n1 = 3 if n1 == 4
194+
195+
# remaining days
196+
days -= (n1 * ANNUAL_DAYS)
197+
198+
# determine the year
199+
year = 1 + (n400 * 400) + (n100 * 100) + (n4 * 4) + n1
200+
201+
# add 1 back to 1-based day_count to determine the month and day
202+
month, day = self.month_and_day(days + 1, year)
203+
[year, month, day]
155204
end
156205

157206
#
@@ -205,14 +254,13 @@ def initialize(year:, month:, day:, ordinal_day: nil)
205254
max_days = Date.month_days(month, year)
206255
raise(InvalidDay, day) unless (MIN_D..max_days).cover?(day)
207256

208-
@leap_year = Date.leap_year?(year)
257+
# initialize
209258
@ordinal_day = ordinal_day || Date.to_ordinal(year, month, day)
210-
211259
super(year:, month:, day:)
212260
end
213261

214262
def leap_year?
215-
@leap_year
263+
Date.leap_year?(year)
216264
end
217265

218266
def <=>(other)

test/bench/date.rb

Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -35,57 +35,50 @@
3535
dates << [year, 8, rand(1..31)]
3636
}
3737

38-
pairs = []
3938

40-
999.times {
41-
pairs << dates.sample(2)
42-
}
39+
# --- Benchmark 1: Object Creation ---
40+
puts "--- Benchmarking Object Creation (new) ---"
41+
d = dates.sample
4342

4443
Benchmark.ips { |b|
4544
b.config(warmup: 0.2, time: 1)
46-
47-
b.report("Ruby Date Difference") {
48-
pairs.each { |(d1, d2)|
49-
rd1 = Date.new(*d1)
50-
rd2 = Date.new(*d2)
51-
rd1 - rd2
52-
}
53-
}
54-
55-
b.report("CompSciDate Difference") {
56-
pairs.each { |(d1, d2)|
57-
cd1 = CompSci::Date.new(*d1)
58-
cd2 = CompSci::Date.new(*d2)
59-
cd2.diff(cd1)
60-
}
61-
}
62-
45+
b.report("Ruby Date new") { Date.new(*d) }
46+
b.report("CompSci::Date new") { CompSci::Date.new(*d) }
6347
b.compare!
6448
}
6549

50+
# --- Benchmark 2: Date Arithmetic (+, -) ---
51+
# Create objects outside the loop so we only measure the arithmetic
52+
puts "\n--- Benchmarking Date Arithmetic (+, -) ---"
53+
rdate = Date.new(2024, 1, 1)
54+
cdate = CompSci::Date.new(year: 2024, month: 1, day: 1)
55+
days = rand(9999)
56+
6657
Benchmark.ips { |b|
6758
b.config(warmup: 0.2, time: 1)
68-
69-
b.report("Ruby Date new") {
70-
rd = Date.new(*dates.sample)
71-
99.times {
72-
rd + rand(9999)
73-
rd - rand(9999)
74-
}
59+
b.report("Ruby Date +/-") {
60+
rdate + days
61+
rdate - days
7562
}
76-
77-
b.report("CompSci::Date new") {
78-
cd = CompSci::Date.new(*dates.sample)
79-
99.times {
80-
val = rand(9999)
81-
begin
82-
cd + val
83-
cd - val
84-
rescue => e
85-
puts format("Exception: %s: %s; %s +- %i", e.class, e.message, cd, val)
86-
end
87-
}
63+
b.report("CompSci::Date +/-") {
64+
cdate + days
65+
cdate - days
8866
}
67+
b.compare!
68+
}
8969

70+
# --- Benchmark 3: Date Difference ---
71+
# Create objects outside the loop so we only measure the difference
72+
puts "\n--- Benchmarking Date Difference ---"
73+
d1, d2 = dates.sample(2)
74+
r1 = Date.new(*d1)
75+
r2 = Date.new(*d2)
76+
c1 = CompSci::Date.new(*d1)
77+
c2 = CompSci::Date.new(*d2)
78+
79+
Benchmark.ips { |b|
80+
b.config(warmup: 0.2, time: 1)
81+
b.report("Ruby Date diff") { r1 - r2 }
82+
b.report("CompSci::Date diff") { c1.diff(c2) }
9083
b.compare!
9184
}

test/bench/date_to_ymd.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require 'benchmark/ips'
2+
require 'compsci/date'
3+
4+
day_counts = []
5+
6+
# Early years (where the leap day pattern hasn't fully established)
7+
(1..100).each { |d| day_counts << d }
8+
(360..400).each { |d| day_counts << d } # Around year 2
9+
(1460..1470).each { |d| day_counts << d } # Around the first 4-year cycle
10+
11+
# Years far in the future (where the error of the linear approx accumulates)
12+
# Day counts for years 8000-9999
13+
(2_922_000..3_652_000).step(1000).each { |d| day_counts << d }
14+
15+
# quick cross-check
16+
day_counts.each { |d|
17+
f = CompSci::Date.to_ymd_flt(d)
18+
i = CompSci::Date.to_ymd_int(d)
19+
20+
if f != i
21+
raise format("flt != int: (%s, %s)", f.join('-'), i.join('-'))
22+
end
23+
}
24+
25+
puts "--- Benchmarking from_ordinal on Edge Case Dates ---"
26+
Benchmark.ips { |b|
27+
b.config(warmup: 0.5, time: 2)
28+
29+
b.report("to_ymd_flt") {
30+
CompSci::Date.to_ymd_flt(day_counts.sample)
31+
}
32+
33+
b.report("to_ymd_int") {
34+
CompSci::Date.to_ymd_int(day_counts.sample)
35+
}
36+
37+
b.compare!
38+
}

0 commit comments

Comments
 (0)