@@ -9,7 +9,8 @@ class Date < Data.define(:year, :month, :day)
9
9
class InvalidYear < RuntimeError ; end
10
10
class InvalidMonth < RuntimeError ; end
11
11
class InvalidDay < RuntimeError ; end
12
-
12
+ class NegativeError < RuntimeError ; end
13
+
13
14
include Comparable
14
15
15
16
#
@@ -45,11 +46,16 @@ def self.leap_days(year)
45
46
# derive CUMULATIVE_DAYS from MONTH_DAYS, zero-indexed
46
47
CUMULATIVE_DAYS = MONTH_DAYS . reduce ( [ 0 ] ) { |acc , days |
47
48
acc + [ acc . last + days ]
48
- } # [0, 31, 59, 90, 120, ... 365]
49
+ } # [0, 31, 59, 90, 120, ... 334, 365]
49
50
ANNUAL_DAYS = CUMULATIVE_DAYS . pop # 365
50
51
LEAP_YEAR_DAYS = ANNUAL_DAYS + 1 # 366
51
52
CUMULATIVE_DAYS . freeze
52
53
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
+
53
59
# implementation considerations
54
60
MIN_Y , MIN_M , MIN_D = 1 , 1 , 1
55
61
MAX_Y , MAX_M , MAX_D = 9999 , MONTH_DAYS . size , MON31
@@ -73,75 +79,38 @@ def self.year_days(years)
73
79
years * ANNUAL_DAYS + self . leap_days ( years )
74
80
end
75
81
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
-
82
82
# perform lookup by month number and year, one-indexed, with leap days
83
83
def self . month_days ( month , year )
84
84
raise ( InvalidMonth , month . inspect ) unless ( 1 ..12 ) . cover? ( month )
85
85
( month == 2 and self . leap_year? ( year ) ) ?
86
86
MON29 : MONTH_DAYS . fetch ( month - 1 )
87
87
end
88
88
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
93
92
end
94
93
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 )
101
97
end
102
-
98
+
103
99
# 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 ] ]
134
104
end
135
-
105
+
136
106
#
137
- # Coversions (days since Epoch, 0001-01-01)
107
+ # Ordinal Coversions (days since Epoch, 0001-01-01)
138
108
#
139
109
140
110
# convert date (as year, month, day) to days since epoch
141
111
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
145
114
end
146
115
147
116
# convert days since epoch back to Date
@@ -152,27 +121,25 @@ def self.from_ordinal(day_count)
152
121
153
122
# use floating point and heuristic, very efficient
154
123
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
159
128
year_days = self . year_days ( year - 1 )
160
129
161
130
# rewind the guess as needed, typically 0 or 1x, rarely 2x
162
131
while year > MIN_Y and year_days >= day_count
163
132
year -= 1
164
- year_days -= self . annual_days ( year )
133
+ year_days -= self . leap_year? ( year ) ? LEAP_YEAR_DAYS : ANNUAL_DAYS
165
134
end
166
135
167
- month , day = self . month_and_day ( day_count - year_days , year )
136
+ month , day = self . month_and_day ( day_count - year_days , year : )
168
137
[ year , month , day ]
169
138
end
170
139
171
140
# use pure divmod arithmetic and integers; constant time; ~same~ efficiency
172
141
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
176
143
177
144
# Convert to 0-based day count for easier math
178
145
days = day_count - 1
@@ -199,7 +166,7 @@ def self.to_ymd_int(day_count)
199
166
year = 1 + ( n400 * 400 ) + ( n100 * 100 ) + ( n4 * 4 ) + n1
200
167
201
168
# 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 : )
203
170
[ year , month , day ]
204
171
end
205
172
0 commit comments