Skip to content

Commit 07aeed3

Browse files
author
Rick Hull
committed
large refactor to remove internal class methods
1 parent c4ef0ba commit 07aeed3

File tree

3 files changed

+57
-96
lines changed

3 files changed

+57
-96
lines changed

lib/compsci/date.rb

Lines changed: 31 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ class Date < Data.define(:year, :month, :day)
99
class InvalidYear < RuntimeError; end
1010
class InvalidMonth < RuntimeError; end
1111
class InvalidDay < RuntimeError; end
12-
12+
class NegativeError < RuntimeError; end
13+
1314
include Comparable
1415

1516
#
@@ -45,11 +46,16 @@ def self.leap_days(year)
4546
# derive CUMULATIVE_DAYS from MONTH_DAYS, zero-indexed
4647
CUMULATIVE_DAYS = MONTH_DAYS.reduce([0]) { |acc, days|
4748
acc + [acc.last + days]
48-
} # [0, 31, 59, 90, 120, ... 365]
49+
} # [0, 31, 59, 90, 120, ... 334, 365]
4950
ANNUAL_DAYS = CUMULATIVE_DAYS.pop # 365
5051
LEAP_YEAR_DAYS = ANNUAL_DAYS + 1 # 366
5152
CUMULATIVE_DAYS.freeze
5253

54+
# derive CUMULATIVE_LEAP_DAYS from CUMULATIVE_DAYS, zero-indexed
55+
CUMULATIVE_LEAP_DAYS = CUMULATIVE_DAYS.map.with_index { |days, i|
56+
i < 2 ? days : days + 1
57+
}.freeze # [0, 31, 60, 91, 121, ... 335]
58+
5359
# implementation considerations
5460
MIN_Y, MIN_M, MIN_D = 1, 1, 1
5561
MAX_Y, MAX_M, MAX_D = 9999, MONTH_DAYS.size, MON31
@@ -73,75 +79,38 @@ def self.year_days(years)
7379
years * ANNUAL_DAYS + self.leap_days(years)
7480
end
7581

76-
# given a day count, what is the current year?
77-
# despite leap years, never guess too low, only too high
78-
def self.guess_year(day_count)
79-
((day_count - 1) / MEAN_ANNUAL_DAYS).round + 1
80-
end
81-
8282
# perform lookup by month number and year, one-indexed, with leap days
8383
def self.month_days(month, year)
8484
raise(InvalidMonth, month.inspect) unless (1..12).cover?(month)
8585
(month == 2 and self.leap_year?(year)) ?
8686
MON29 : MONTH_DAYS.fetch(month - 1)
8787
end
8888

89-
# given an annual day count, what is the current month?
90-
# despite leap years, never guess too low, only too high
91-
def self.guess_month(day_of_year)
92-
(day_of_year / MON30 + 1).clamp(MIN_M, MAX_M)
89+
# select the table based on leap year status
90+
def self.cumulative_table(year)
91+
self.leap_year?(year) ? CUMULATIVE_LEAP_DAYS : CUMULATIVE_DAYS
9392
end
9493

95-
# how many days have elapsed before the beginning of the month?
96-
# perform lookup by month number and year, one-indexed, with leap days
97-
def self.cumulative_days(month, year)
98-
raise(InvalidMonth, month.inspect) unless (1..12).cover?(month)
99-
days = CUMULATIVE_DAYS.fetch(month - 1)
100-
(month > 2 and self.leap_year?(year)) ? (days + 1) : days
94+
# fetch from the appropriate table, one-indexed
95+
def self.cumulative_days(month, year:)
96+
self.cumulative_table(year).fetch(month - 1)
10197
end
102-
98+
10399
# given number of days, what is the current month and day
104-
def self.month_and_day(day_of_year, year)
105-
month = self.guess_month(day_of_year)
106-
month_days = self.cumulative_days(month, year)
107-
108-
# rewind the guess by one month if needed
109-
if month > MIN_M and month_days >= day_of_year
110-
month -= 1
111-
month_days = self.cumulative_days(month, year)
112-
end
113-
114-
[month, day_of_year - month_days]
115-
end
116-
117-
# how many days in a given year?
118-
def self.annual_days(year)
119-
self.leap_year?(year) ? LEAP_YEAR_DAYS : ANNUAL_DAYS
120-
end
121-
122-
# convert days to current year with days remaining
123-
def self.year_and_day(day_count)
124-
year = self.guess_year(day_count)
125-
year_days = self.year_days(year - 1)
126-
127-
# rewind the guess as needed, typically 0 or 1x, rarely 2x
128-
while year > MIN_Y and year_days >= day_count
129-
year -= 1
130-
year_days -= self.annual_days(year)
131-
end
132-
133-
[year, day_count - year_days]
100+
def self.month_and_day(day_of_year, year:)
101+
tbl = self.cumulative_table(year)
102+
idx = tbl.rindex { |c| c < day_of_year }
103+
[idx + 1, day_of_year - tbl[idx]]
134104
end
135-
105+
136106
#
137-
# Coversions (days since Epoch, 0001-01-01)
107+
# Ordinal Coversions (days since Epoch, 0001-01-01)
138108
#
139109

140110
# convert date (as year, month, day) to days since epoch
141111
def self.to_ordinal(year, month, day)
142-
self.year_days(year - 1) +
143-
self.cumulative_days(month, year) +
144-
day
112+
raise(InvalidMonth, month.inspect) unless (1..12).cover?(month)
113+
self.year_days(year - 1) + self.cumulative_days(month, year:) + day
145114
end
146115

147116
# convert days since epoch back to Date
@@ -152,27 +121,25 @@ def self.from_ordinal(day_count)
152121

153122
# use floating point and heuristic, very efficient
154123
def self.to_ymd_flt(day_count)
155-
unless day_count > 0
156-
raise(RuntimeError, "day_count should be positive: #{day_count}")
157-
end
158-
year = self.guess_year(day_count)
124+
raise(NegativeError, day_count.to_s) unless day_count > 0
125+
126+
# this is a simple linear approximation; a guess
127+
year = ((day_count - 1) / MEAN_ANNUAL_DAYS).round + 1
159128
year_days = self.year_days(year - 1)
160129

161130
# rewind the guess as needed, typically 0 or 1x, rarely 2x
162131
while year > MIN_Y and year_days >= day_count
163132
year -= 1
164-
year_days -= self.annual_days(year)
133+
year_days -= self.leap_year?(year) ? LEAP_YEAR_DAYS : ANNUAL_DAYS
165134
end
166135

167-
month, day = self.month_and_day(day_count - year_days, year)
136+
month, day = self.month_and_day(day_count - year_days, year:)
168137
[year, month, day]
169138
end
170139

171140
# use pure divmod arithmetic and integers; constant time; ~same~ efficiency
172141
def self.to_ymd_int(day_count)
173-
unless day_count > 0
174-
raise(RuntimeError, "day_count should be positive: #{day_count}")
175-
end
142+
raise(NegativeError, day_count.to_s) unless day_count > 0
176143

177144
# Convert to 0-based day count for easier math
178145
days = day_count - 1
@@ -199,7 +166,7 @@ def self.to_ymd_int(day_count)
199166
year = 1 + (n400 * 400) + (n100 * 100) + (n4 * 4) + n1
200167

201168
# add 1 back to 1-based day_count to determine the month and day
202-
month, day = self.month_and_day(days + 1, year)
169+
month, day = self.month_and_day(days + 1, year:)
203170
[year, month, day]
204171
end
205172

test/bench/date.rb

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@
3636
}
3737

3838

39-
# --- Benchmark 1: Object Creation ---
40-
puts "--- Benchmarking Object Creation (new) ---"
39+
#
40+
# Benchmark 1: Object Creation
41+
#
42+
4143
d = dates.sample
4244

4345
Benchmark.ips { |b|
@@ -47,9 +49,11 @@
4749
b.compare!
4850
}
4951

50-
# --- Benchmark 2: Date Arithmetic (+, -) ---
51-
# Create objects outside the loop so we only measure the arithmetic
52-
puts "\n--- Benchmarking Date Arithmetic (+, -) ---"
52+
53+
#
54+
# Benchmark 2: Date Arithmetic
55+
#
56+
5357
rdate = Date.new(2024, 1, 1)
5458
cdate = CompSci::Date.new(year: 2024, month: 1, day: 1)
5559
days = rand(9999)
@@ -67,9 +71,11 @@
6771
b.compare!
6872
}
6973

70-
# --- Benchmark 3: Date Difference ---
71-
# Create objects outside the loop so we only measure the difference
72-
puts "\n--- Benchmarking Date Difference ---"
74+
75+
#
76+
# Benchmark 3: Date Difference
77+
#
78+
7379
d1, d2 = dates.sample(2)
7480
r1 = Date.new(*d1)
7581
r2 = Date.new(*d2)
@@ -82,3 +88,15 @@
8288
b.report("CompSci::Date diff") { c1.diff(c2) }
8389
b.compare!
8490
}
91+
92+
93+
#
94+
# Benchmark 4: Date comparison
95+
#
96+
97+
Benchmark.ips { |b|
98+
b.config(warmup: 0.2, time: 1)
99+
b.report("Ruby Date comparison") { r1 <=> r2 }
100+
b.report("CompSci::Date comparison") { c1 <=> c2 }
101+
b.compare!
102+
}

test/date.rb

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,6 @@
5757
expect(D.leap_days(100)).must_equal 24 # no leap day for year 100
5858
expect(D.leap_days(400)).must_equal 97 # includes leap day for year 400
5959
end
60-
61-
it "determines the number of days in any given year" do
62-
# 0 is not valid but the function will accept it
63-
expect(D.annual_days 0).must_equal 366
64-
expect(D.annual_days 1).must_equal 365
65-
expect(D.annual_days 2).must_equal 365
66-
expect(D.annual_days 3).must_equal 365
67-
expect(D.annual_days 4).must_equal 366
68-
expect(D.annual_days 5).must_equal 365
69-
end
7060
end
7161

7262
describe "month and day calculations" do
@@ -97,20 +87,6 @@
9787
expect { D.month_name(0) }.must_raise
9888
expect { D.month_name(13) }.must_raise
9989
end
100-
101-
it "looks up accumulated annual days for a given month and year" do
102-
# january is always 0
103-
expect(D.cumulative_days(1, 2025)).must_equal 0
104-
expect(D.cumulative_days(1, 2020)).must_equal 0
105-
106-
# february is always 31
107-
expect(D.cumulative_days(2, 2025)).must_equal 31
108-
expect(D.cumulative_days(2, 2020)).must_equal 31
109-
110-
# march-december can vary +1 on leap year
111-
expect(D.cumulative_days(3, 2025)).must_equal 31 + 28
112-
expect(D.cumulative_days(3, 2020)).must_equal 31 + 29
113-
end
11490
end
11591

11692
describe "date validation and initialization" do

0 commit comments

Comments
 (0)