Skip to content

Commit b76e7e6

Browse files
author
Rick Hull
committed
fix bug in guess_year
- rename day_count to ordinal_day - associated renamings
1 parent afd6ca7 commit b76e7e6

File tree

3 files changed

+63
-55
lines changed

3 files changed

+63
-55
lines changed

lib/compsci/date.rb

Lines changed: 52 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ class InvalidDay < RuntimeError; end
1212

1313
include Comparable
1414

15-
#
15+
#
1616
# Leap Years
1717
#
18-
18+
1919
# this is the crux of the Gregorian calendar
2020
def self.leap_year?(year)
2121
(year % 4).zero? and !(year % 100).zero? or (year % 400).zero?
@@ -42,7 +42,7 @@ def self.leap_days(year)
4242
[MON31, MON28, MON31, MON30, MON31, MON30,
4343
MON31, MON31, MON30, MON31, MON30, MON31].freeze
4444
NUM_MONTHS = 12 # MONTH_DAYS.size
45-
45+
4646
# derive CUMULATIVE_DAYS from MONTH_DAYS, zero-indexed
4747
CUMULATIVE_DAYS = MONTH_DAYS.reduce([0]) { |acc, days|
4848
acc + [acc.last + days]
@@ -55,28 +55,44 @@ def self.leap_days(year)
5555
MIN_Y, MIN_M, MIN_D = 1, 1, 1
5656
MAX_Y, MAX_M, MAX_D = 9999, NUM_MONTHS, MON31
5757

58+
DAYS_400 = 146097 # self.year_days(400)
59+
DAYS_100 = 36524 # self.year_days(100)
60+
DAYS_4 = 1461 # self.year_days(4)
61+
62+
5863
# currently unused
5964
# EPOCH_Y, EPOCH_M, EPOCH_D = 1, 1, 1
60-
# MEAN_ANNUAL_DAYS = 365.2425
65+
MEAN_ANNUAL_DAYS = 365.2425 # DAYS_400 / 400.0
6166
# MEAN_MONTH_DAYS = MEAN_ANNUAL_DAYS / NUM_MONTHS
62-
67+
6368
#
6469
# Functions
6570
#
66-
71+
72+
# how many days in the years since epoch
73+
def self.year_days(years)
74+
years * ANNUAL_DAYS + self.leap_days(years)
75+
end
76+
77+
# given a day count, what is the current year?
78+
# despite leap years, never guess too low, only too high
79+
def self.guess_year(days)
80+
((days - 1) / MEAN_ANNUAL_DAYS).round + 1
81+
end
82+
6783
# perform lookup by month number and year, one-indexed, with leap days
6884
def self.month_days(month, year)
6985
raise(InvalidMonth, month.inspect) unless (1..12).cover?(month)
7086
(month == 2 and self.leap_year?(year)) ?
7187
MON29 : MONTH_DAYS.fetch(month - 1)
7288
end
73-
74-
# given a day count, what is the current month?
89+
90+
# given an annual day count, what is the current month?
7591
# despite leap years, never guess too low, only too high
76-
def self.guess_month(days)
77-
(days / MON30 + 1).clamp(MIN_M, MAX_M)
92+
def self.guess_month(day_of_year)
93+
(day_of_year / MON30 + 1).clamp(MIN_M, MAX_M)
7894
end
79-
95+
8096
# how many days have elapsed before the beginning of the month?
8197
# perform lookup by month number and year, one-indexed, with leap days
8298
def self.cumulative_days(month, year)
@@ -86,39 +102,26 @@ def self.cumulative_days(month, year)
86102
end
87103

88104
# given number of days, what is the current month and day
89-
def self.rev_cumulative(days, year)
90-
month = self.guess_month(days)
105+
def self.month_and_day(day_of_year, year)
106+
month = self.guess_month(day_of_year)
91107
month_days = self.cumulative_days(month, year)
92108

93109
# rewind the guess by one month if needed
94-
if month > 1 and month_days >= days
110+
if month > 1 and month_days >= day_of_year
95111
month -= 1
96112
month_days = self.cumulative_days(month, year)
97113
end
98114

99-
[month, days - month_days]
115+
[month, day_of_year - month_days]
100116
end
101117

102-
# given a day count, what is the current year?
103-
# despite leap years, never guess too low, only too high
104-
# mostly accurate, errs by 1 at most
105-
def self.guess_year(days)
106-
bias = 2 # minimum required to meet guarantees above
107-
((days + bias) / ANNUAL_DAYS + 1).clamp(MIN_Y, MAX_Y)
108-
end
109-
110-
# how many days in the years since epoch
111-
def self.year_days(years)
112-
years * ANNUAL_DAYS + self.leap_days(years)
113-
end
114-
115118
# how many days in a given year?
116119
def self.annual_days(year)
117120
self.leap_year?(year) ? LEAP_YEAR_DAYS : ANNUAL_DAYS
118121
end
119-
122+
120123
# convert days to current year with days remaining
121-
def self.year_count(days)
124+
def self.year_and_day(days)
122125
year = self.guess_year(days)
123126
year_days = self.year_days(year - 1)
124127

@@ -136,20 +139,20 @@ def self.year_count(days)
136139
#
137140

138141
# convert date (as year, month, day) to days since epoch
139-
def self.day_count(year, month, day)
142+
def self.to_ordinal(year, month, day)
140143
self.year_days(year - 1) +
141144
self.cumulative_days(month, year) +
142145
day
143146
end
144147

145148
# convert days since epoch back to Date
146-
def self.from_days(days)
149+
def self.from_ordinal(days)
147150
raise(RuntimeError, "days should be positive: #{days}") unless days > 0
148-
year, days = self.year_count(days)
149-
month, day = self.rev_cumulative(days, year)
151+
year, days = self.year_and_day(days)
152+
month, day = self.month_and_day(days, year)
150153
Date.new(year, month, day)
151154
end
152-
155+
153156
#
154157
# Month Names
155158
#
@@ -162,13 +165,13 @@ def self.from_days(days)
162165
MONTH_NUMS = MONTH_NAMES.each.with_index.to_h { |name, i|
163166
[name.downcase.to_sym, i + 1]
164167
}.freeze
165-
168+
166169
# perform lookup by month name, one-indexed
167170
def self.month_number(name)
168171
name = name.downcase.to_sym if name.is_a? String
169172
MONTH_NUMS.fetch name
170173
end
171-
174+
172175
# perform lookup of month name by month number, one-indexed
173176
def self.month_name(number)
174177
raise(InvalidMonth, number.inspect) unless (1..12).cover?(number)
@@ -178,13 +181,13 @@ def self.month_name(number)
178181
#
179182
# Date Instances
180183
#
181-
182-
attr_reader :day_count
183-
184+
185+
attr_reader :ordinal_day
186+
184187
def initialize(year:, month:, day:)
185188
# validate year
186189
raise(InvalidYear, year) unless (MIN_Y..MAX_Y).cover?(year)
187-
190+
188191
# handle month conversion
189192
case month
190193
when Integer
@@ -196,23 +199,23 @@ def initialize(year:, month:, day:)
196199
raise InvalidMonth, month.inspect
197200
end
198201
end
199-
202+
200203
# validate day
201204
max_days = Date.month_days(month, year)
202205
raise(InvalidDay, day) unless (MIN_D..max_days).cover?(day)
203-
206+
204207
@leap_year = Date.leap_year?(year)
205-
@day_count = Date.day_count(year, month, day)
206-
208+
@ordinal_day = Date.to_ordinal(year, month, day)
209+
207210
super(year:, month:, day:)
208211
end
209-
212+
210213
def leap_year?
211214
@leap_year
212215
end
213216

214217
def <=>(other)
215-
@day_count <=> other.day_count
218+
@ordinal_day <=> other.ordinal_day
216219
end
217220

218221
def to_s
@@ -226,7 +229,7 @@ def name
226229
# given a count of days, return a new Date
227230
def +(days)
228231
return self if days.zero?
229-
Date.from_days(@day_count + days)
232+
Date.from_ordinal(@ordinal_day + days)
230233
end
231234

232235
def -(days)
@@ -235,7 +238,7 @@ def -(days)
235238

236239
# given a Date, return a count of days, possibly negative
237240
def diff(other)
238-
@day_count - other.day_count
241+
@ordinal_day - other.ordinal_day
239242
end
240243
end
241244
end

test/bench/date.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,13 @@
7777
b.report("CompSci::Date new") {
7878
cd = CompSci::Date.new(*dates.sample)
7979
99.times {
80-
cd + rand(9999)
81-
cd - rand(9999)
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
8287
}
8388
}
8489

test/date.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -141,18 +141,18 @@
141141
end
142142
end
143143

144-
describe "day count conversions" do
144+
describe "ordinal day conversions" do
145145
it "maintains round-trip consistency" do
146146
TEST_DATES.each do |date|
147-
reconstructed = D.from_days(date.day_count)
147+
reconstructed = D.from_ordinal(date.ordinal_day)
148148
expect(reconstructed).must_equal date,
149149
"Failed round-trip for #{date}: got #{reconstructed}"
150150
end
151151
end
152152

153153
it "calculates epoch correctly" do
154-
expect(EPOCH.day_count).must_equal 1
155-
expect(D.from_days(1)).must_equal EPOCH
154+
expect(EPOCH.ordinal_day).must_equal 1
155+
expect(D.from_ordinal(1)).must_equal EPOCH
156156
end
157157
end
158158

0 commit comments

Comments
 (0)