@@ -12,10 +12,10 @@ class InvalidDay < RuntimeError; end
12
12
13
13
include Comparable
14
14
15
- #
15
+ #
16
16
# Leap Years
17
17
#
18
-
18
+
19
19
# this is the crux of the Gregorian calendar
20
20
def self . leap_year? ( year )
21
21
( year % 4 ) . zero? and !( year % 100 ) . zero? or ( year % 400 ) . zero?
@@ -42,7 +42,7 @@ def self.leap_days(year)
42
42
[ MON31 , MON28 , MON31 , MON30 , MON31 , MON30 ,
43
43
MON31 , MON31 , MON30 , MON31 , MON30 , MON31 ] . freeze
44
44
NUM_MONTHS = 12 # MONTH_DAYS.size
45
-
45
+
46
46
# derive CUMULATIVE_DAYS from MONTH_DAYS, zero-indexed
47
47
CUMULATIVE_DAYS = MONTH_DAYS . reduce ( [ 0 ] ) { |acc , days |
48
48
acc + [ acc . last + days ]
@@ -55,28 +55,44 @@ def self.leap_days(year)
55
55
MIN_Y , MIN_M , MIN_D = 1 , 1 , 1
56
56
MAX_Y , MAX_M , MAX_D = 9999 , NUM_MONTHS , MON31
57
57
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
+
58
63
# currently unused
59
64
# 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
61
66
# MEAN_MONTH_DAYS = MEAN_ANNUAL_DAYS / NUM_MONTHS
62
-
67
+
63
68
#
64
69
# Functions
65
70
#
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
+
67
83
# perform lookup by month number and year, one-indexed, with leap days
68
84
def self . month_days ( month , year )
69
85
raise ( InvalidMonth , month . inspect ) unless ( 1 ..12 ) . cover? ( month )
70
86
( month == 2 and self . leap_year? ( year ) ) ?
71
87
MON29 : MONTH_DAYS . fetch ( month - 1 )
72
88
end
73
-
74
- # given a day count, what is the current month?
89
+
90
+ # given an annual day count, what is the current month?
75
91
# 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 )
78
94
end
79
-
95
+
80
96
# how many days have elapsed before the beginning of the month?
81
97
# perform lookup by month number and year, one-indexed, with leap days
82
98
def self . cumulative_days ( month , year )
@@ -86,39 +102,26 @@ def self.cumulative_days(month, year)
86
102
end
87
103
88
104
# 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 )
91
107
month_days = self . cumulative_days ( month , year )
92
108
93
109
# 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
95
111
month -= 1
96
112
month_days = self . cumulative_days ( month , year )
97
113
end
98
114
99
- [ month , days - month_days ]
115
+ [ month , day_of_year - month_days ]
100
116
end
101
117
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
-
115
118
# how many days in a given year?
116
119
def self . annual_days ( year )
117
120
self . leap_year? ( year ) ? LEAP_YEAR_DAYS : ANNUAL_DAYS
118
121
end
119
-
122
+
120
123
# convert days to current year with days remaining
121
- def self . year_count ( days )
124
+ def self . year_and_day ( days )
122
125
year = self . guess_year ( days )
123
126
year_days = self . year_days ( year - 1 )
124
127
@@ -136,20 +139,20 @@ def self.year_count(days)
136
139
#
137
140
138
141
# 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 )
140
143
self . year_days ( year - 1 ) +
141
144
self . cumulative_days ( month , year ) +
142
145
day
143
146
end
144
147
145
148
# convert days since epoch back to Date
146
- def self . from_days ( days )
149
+ def self . from_ordinal ( days )
147
150
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 )
150
153
Date . new ( year , month , day )
151
154
end
152
-
155
+
153
156
#
154
157
# Month Names
155
158
#
@@ -162,13 +165,13 @@ def self.from_days(days)
162
165
MONTH_NUMS = MONTH_NAMES . each . with_index . to_h { |name , i |
163
166
[ name . downcase . to_sym , i + 1 ]
164
167
} . freeze
165
-
168
+
166
169
# perform lookup by month name, one-indexed
167
170
def self . month_number ( name )
168
171
name = name . downcase . to_sym if name . is_a? String
169
172
MONTH_NUMS . fetch name
170
173
end
171
-
174
+
172
175
# perform lookup of month name by month number, one-indexed
173
176
def self . month_name ( number )
174
177
raise ( InvalidMonth , number . inspect ) unless ( 1 ..12 ) . cover? ( number )
@@ -178,13 +181,13 @@ def self.month_name(number)
178
181
#
179
182
# Date Instances
180
183
#
181
-
182
- attr_reader :day_count
183
-
184
+
185
+ attr_reader :ordinal_day
186
+
184
187
def initialize ( year :, month :, day :)
185
188
# validate year
186
189
raise ( InvalidYear , year ) unless ( MIN_Y ..MAX_Y ) . cover? ( year )
187
-
190
+
188
191
# handle month conversion
189
192
case month
190
193
when Integer
@@ -196,23 +199,23 @@ def initialize(year:, month:, day:)
196
199
raise InvalidMonth , month . inspect
197
200
end
198
201
end
199
-
202
+
200
203
# validate day
201
204
max_days = Date . month_days ( month , year )
202
205
raise ( InvalidDay , day ) unless ( MIN_D ..max_days ) . cover? ( day )
203
-
206
+
204
207
@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
+
207
210
super ( year :, month :, day :)
208
211
end
209
-
212
+
210
213
def leap_year?
211
214
@leap_year
212
215
end
213
216
214
217
def <=>( other )
215
- @day_count <=> other . day_count
218
+ @ordinal_day <=> other . ordinal_day
216
219
end
217
220
218
221
def to_s
@@ -226,7 +229,7 @@ def name
226
229
# given a count of days, return a new Date
227
230
def +( days )
228
231
return self if days . zero?
229
- Date . from_days ( @day_count + days )
232
+ Date . from_ordinal ( @ordinal_day + days )
230
233
end
231
234
232
235
def -( days )
@@ -235,7 +238,7 @@ def -(days)
235
238
236
239
# given a Date, return a count of days, possibly negative
237
240
def diff ( other )
238
- @day_count - other . day_count
241
+ @ordinal_day - other . ordinal_day
239
242
end
240
243
end
241
244
end
0 commit comments