fat_core 5.6.1 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +0 -4
- data/README.org +38 -409
- data/bin/console +1 -3
- data/lib/fat_core/all.rb +0 -1
- data/lib/fat_core/string.rb +0 -20
- data/lib/fat_core/version.rb +3 -3
- data/lib/fat_core.rb +0 -2
- data/spec/lib/range_spec.rb +91 -90
- data/spec/lib/string_spec.rb +0 -9
- metadata +4 -8
- data/bin/easter +0 -45
- data/lib/fat_core/date.rb +0 -1897
- data/spec/lib/date_spec.rb +0 -1390
data/lib/fat_core/date.rb
DELETED
|
@@ -1,1897 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'active_support/core_ext/date'
|
|
4
|
-
require 'active_support/core_ext/time'
|
|
5
|
-
require 'active_support/core_ext/numeric/time'
|
|
6
|
-
require 'active_support/core_ext/integer/time'
|
|
7
|
-
require 'fat_core/string'
|
|
8
|
-
require 'fat_core/patches'
|
|
9
|
-
|
|
10
|
-
module FatCore
|
|
11
|
-
# ## FatCore Date Extensions
|
|
12
|
-
#
|
|
13
|
-
# The FatCore extensions to the Date class add the notion of several additional
|
|
14
|
-
# calendar periods besides years, months, and weeks to those provided for in the
|
|
15
|
-
# Date class and the active_support extensions to Date. In particular, there
|
|
16
|
-
# are several additional calendar subdivisions (called "chunks" in this
|
|
17
|
-
# documentation) supported by FatCore's extension to the Date class:
|
|
18
|
-
#
|
|
19
|
-
# * year,
|
|
20
|
-
# * half,
|
|
21
|
-
# * quarter,
|
|
22
|
-
# * bimonth,
|
|
23
|
-
# * month,
|
|
24
|
-
# * semimonth,
|
|
25
|
-
# * biweek,
|
|
26
|
-
# * week, and
|
|
27
|
-
# * day
|
|
28
|
-
#
|
|
29
|
-
# For each of those chunks, there are methods for finding the beginning and end
|
|
30
|
-
# of the chunk, for advancing or retreating a Date by the chunk, and for testing
|
|
31
|
-
# whether a Date is at the beginning or end of each of the chunk.
|
|
32
|
-
#
|
|
33
|
-
# FatCore's Date extension defines a few convenience formatting methods, such as
|
|
34
|
-
# Date#iso and Date#org for formatting Dates as ISO strings and as Emacs
|
|
35
|
-
# org-mode inactive timestamps respectively. It also has a few utility methods
|
|
36
|
-
# for determining the date of Easter, the number of days in any given month, and
|
|
37
|
-
# the Date of the nth workday in a given month (say the third Thursday in
|
|
38
|
-
# October, 2014).
|
|
39
|
-
#
|
|
40
|
-
# The Date extension defines a couple of class methods for parsing strings into
|
|
41
|
-
# Dates, especially Date.parse_spec, which allows Dates to be specified in a
|
|
42
|
-
# lazy way, either absolutely or relative to the computer's clock.
|
|
43
|
-
#
|
|
44
|
-
# Finally FatCore's Date extensions provide thorough methods for determining if
|
|
45
|
-
# a Date is a United States federal holiday or workday based on US law,
|
|
46
|
-
# including executive orders. It does the same for the New York Stock Exchange,
|
|
47
|
-
# based on the rules of the New York Stock Exchange, including dates on which
|
|
48
|
-
# the NYSE was closed for special reasons, such as the 9-11 attacks in 2001.
|
|
49
|
-
module Date
|
|
50
|
-
# Constant for Beginning of Time (BOT) outside the range of what we would ever
|
|
51
|
-
# want to find in commercial situations.
|
|
52
|
-
BOT = ::Date.parse('1900-01-01')
|
|
53
|
-
|
|
54
|
-
# Constant for End of Time (EOT) outside the range of what we would ever want
|
|
55
|
-
# to find in commercial situations.
|
|
56
|
-
EOT = ::Date.parse('3000-12-31')
|
|
57
|
-
|
|
58
|
-
# Symbols for each of the days of the week, e.g., :sunday, :monday, etc.
|
|
59
|
-
DAYSYMS = ::Date::DAYNAMES.map { |name| name.downcase.to_sym }
|
|
60
|
-
|
|
61
|
-
# :category: Formatting
|
|
62
|
-
# @group Formatting
|
|
63
|
-
|
|
64
|
-
# Format as an ISO string of the form `YYYY-MM-DD`.
|
|
65
|
-
# @return [String]
|
|
66
|
-
def iso
|
|
67
|
-
strftime('%Y-%m-%d')
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# :category: Formatting
|
|
71
|
-
|
|
72
|
-
# Format date to TeX documents as ISO strings but with en-dashes
|
|
73
|
-
# @return [String]
|
|
74
|
-
def tex_quote
|
|
75
|
-
strftime('%Y--%m--%d').tex_quote
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# :category: Formatting
|
|
79
|
-
|
|
80
|
-
# Format as an all-numeric string of the form `YYYYMMDD`
|
|
81
|
-
# @return [String]
|
|
82
|
-
def num
|
|
83
|
-
strftime('%Y%m%d')
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
# :category: Formatting
|
|
87
|
-
|
|
88
|
-
# Format as an inactive Org date timestamp of the form `[YYYY-MM-DD <dow>]`
|
|
89
|
-
# (see Emacs org-mode)
|
|
90
|
-
# @return [String]
|
|
91
|
-
def org(active: false)
|
|
92
|
-
if active
|
|
93
|
-
strftime('<%Y-%m-%d %a>')
|
|
94
|
-
else
|
|
95
|
-
strftime('[%Y-%m-%d %a]')
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# :category: Formatting
|
|
100
|
-
|
|
101
|
-
# Format as an English string, like `'January 12, 2016'`
|
|
102
|
-
# @return [String]
|
|
103
|
-
def eng
|
|
104
|
-
strftime('%B %-d, %Y')
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# :category: Formatting
|
|
108
|
-
|
|
109
|
-
# Format date in `MM/DD/YYYY` form, as typical for the short American
|
|
110
|
-
# form.
|
|
111
|
-
# @return [String]
|
|
112
|
-
def american
|
|
113
|
-
strftime('%-m/%-d/%Y')
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# :category: Queries
|
|
117
|
-
# @group Queries
|
|
118
|
-
|
|
119
|
-
# :category: Queries
|
|
120
|
-
|
|
121
|
-
# Number of days in self's month
|
|
122
|
-
# @return [Integer]
|
|
123
|
-
def days_in_month
|
|
124
|
-
self.class.days_in_month(year, month)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
# :category: Queries
|
|
128
|
-
# @group Queries
|
|
129
|
-
|
|
130
|
-
# :category: Queries
|
|
131
|
-
|
|
132
|
-
# Self's calendar "half" by analogy to calendar quarters: 1 or 2, depending
|
|
133
|
-
# on whether the date falls in the first or second half of the calendar
|
|
134
|
-
# year.
|
|
135
|
-
# @return [Integer]
|
|
136
|
-
def half
|
|
137
|
-
case month
|
|
138
|
-
when (1..6)
|
|
139
|
-
1
|
|
140
|
-
when (7..12)
|
|
141
|
-
2
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# :category: Queries
|
|
146
|
-
|
|
147
|
-
# Self's calendar quarter: 1, 2, 3, or 4, depending on which calendar quarter
|
|
148
|
-
# the date falls in.
|
|
149
|
-
# @return [Integer]
|
|
150
|
-
def quarter
|
|
151
|
-
case month
|
|
152
|
-
when (1..3)
|
|
153
|
-
1
|
|
154
|
-
when (4..6)
|
|
155
|
-
2
|
|
156
|
-
when (7..9)
|
|
157
|
-
3
|
|
158
|
-
when (10..12)
|
|
159
|
-
4
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
# Self's calendar bimonth: 1, 2, 3, 4, 5, or 6 depending on which calendar
|
|
164
|
-
# bimonth the date falls in.
|
|
165
|
-
# @return [Integer]
|
|
166
|
-
def bimonth
|
|
167
|
-
case month
|
|
168
|
-
when (1..2)
|
|
169
|
-
1
|
|
170
|
-
when (3..4)
|
|
171
|
-
2
|
|
172
|
-
when (5..6)
|
|
173
|
-
3
|
|
174
|
-
when (7..8)
|
|
175
|
-
4
|
|
176
|
-
when (9..10)
|
|
177
|
-
5
|
|
178
|
-
when (11..12)
|
|
179
|
-
6
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Self's calendar semimonth: 1, through 24 depending on which calendar
|
|
184
|
-
# semimonth the date falls in.
|
|
185
|
-
# @return [Integer]
|
|
186
|
-
def semimonth
|
|
187
|
-
((month - 1) * 2) + (day <= 15 ? 1 : 2)
|
|
188
|
-
end
|
|
189
|
-
|
|
190
|
-
# Self's calendar biweek: 1, through 24 depending on which calendar
|
|
191
|
-
# semimonth the date falls in.
|
|
192
|
-
# @return [Integer]
|
|
193
|
-
def biweek
|
|
194
|
-
if cweek.odd?
|
|
195
|
-
(cweek + 1) / 2
|
|
196
|
-
else
|
|
197
|
-
cweek / 2
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
# Self's calendar week: just calls cweek.
|
|
202
|
-
# @return [Integer]
|
|
203
|
-
def week
|
|
204
|
-
cweek
|
|
205
|
-
end
|
|
206
|
-
|
|
207
|
-
# :category: Queries
|
|
208
|
-
|
|
209
|
-
# Return whether the date falls on the first day of a year.
|
|
210
|
-
# @return [Boolean]
|
|
211
|
-
def beginning_of_year?
|
|
212
|
-
beginning_of_year == self
|
|
213
|
-
end
|
|
214
|
-
|
|
215
|
-
# :category: Queries
|
|
216
|
-
|
|
217
|
-
# Return whether the date falls on the last day of a year.
|
|
218
|
-
# @return [Boolean]
|
|
219
|
-
def end_of_year?
|
|
220
|
-
end_of_year == self
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# :category: Queries
|
|
224
|
-
|
|
225
|
-
# Return whether the date falls on the first day of a half-year.
|
|
226
|
-
# @return [Boolean]
|
|
227
|
-
def beginning_of_half?
|
|
228
|
-
beginning_of_half == self
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# :category: Queries
|
|
232
|
-
|
|
233
|
-
# Return whether the date falls on the last day of a half-year.
|
|
234
|
-
# @return [Boolean]
|
|
235
|
-
def end_of_half?
|
|
236
|
-
end_of_half == self
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# :category: Queries
|
|
240
|
-
|
|
241
|
-
# Return whether the date falls on the first day of a calendar quarter.
|
|
242
|
-
# @return [Boolean]
|
|
243
|
-
def beginning_of_quarter?
|
|
244
|
-
beginning_of_quarter == self
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
# :category: Queries
|
|
248
|
-
|
|
249
|
-
# Return whether the date falls on the last day of a calendar quarter.
|
|
250
|
-
# @return [Boolean]
|
|
251
|
-
def end_of_quarter?
|
|
252
|
-
end_of_quarter == self
|
|
253
|
-
end
|
|
254
|
-
|
|
255
|
-
# :category: Queries
|
|
256
|
-
|
|
257
|
-
# Return whether the date falls on the first day of a calendar bi-monthly
|
|
258
|
-
# period, i.e., the beginning of an odd-numbered month.
|
|
259
|
-
# @return [Boolean]
|
|
260
|
-
def beginning_of_bimonth?
|
|
261
|
-
month.odd? && beginning_of_month == self
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
# :category: Queries
|
|
265
|
-
|
|
266
|
-
# Return whether the date falls on the last day of a calendar bi-monthly
|
|
267
|
-
# period, i.e., the end of an even-numbered month.
|
|
268
|
-
# @return [Boolean]
|
|
269
|
-
def end_of_bimonth?
|
|
270
|
-
month.even? && end_of_month == self
|
|
271
|
-
end
|
|
272
|
-
|
|
273
|
-
# :category: Queries
|
|
274
|
-
|
|
275
|
-
# Return whether the date falls on the first day of a calendar month.
|
|
276
|
-
# @return [Boolean]
|
|
277
|
-
def beginning_of_month?
|
|
278
|
-
beginning_of_month == self
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
# :category: Queries
|
|
282
|
-
|
|
283
|
-
# Return whether the date falls on the last day of a calendar month.
|
|
284
|
-
# @return [Boolean]
|
|
285
|
-
def end_of_month?
|
|
286
|
-
end_of_month == self
|
|
287
|
-
end
|
|
288
|
-
|
|
289
|
-
# :category: Queries
|
|
290
|
-
|
|
291
|
-
# Return whether the date falls on the first day of a calendar semi-monthly
|
|
292
|
-
# period, i.e., on the 1st or 15th of a month.
|
|
293
|
-
# @return [Boolean]
|
|
294
|
-
def beginning_of_semimonth?
|
|
295
|
-
beginning_of_semimonth == self
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
# :category: Queries
|
|
299
|
-
|
|
300
|
-
# Return whether the date falls on the last day of a calendar semi-monthly
|
|
301
|
-
# period, i.e., on the 14th or the last day of a month.
|
|
302
|
-
# @return [Boolean]
|
|
303
|
-
def end_of_semimonth?
|
|
304
|
-
end_of_semimonth == self
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
# :category: Queries
|
|
308
|
-
|
|
309
|
-
# Return whether the date falls on the first day of a commercial bi-week,
|
|
310
|
-
# i.e., on /Monday/ in a commercial week that is an odd-numbered week. From
|
|
311
|
-
# ::Date: "The calendar week is a seven day period within a calendar year,
|
|
312
|
-
# starting on a Monday and identified by its ordinal number within the year;
|
|
313
|
-
# the first calendar week of the year is the one that includes the first
|
|
314
|
-
# Thursday of that year. In the Gregorian calendar, this is equivalent to
|
|
315
|
-
# the week which includes January 4."
|
|
316
|
-
# @return [Boolean]
|
|
317
|
-
def beginning_of_biweek?
|
|
318
|
-
beginning_of_biweek == self
|
|
319
|
-
end
|
|
320
|
-
|
|
321
|
-
# :category: Queries
|
|
322
|
-
|
|
323
|
-
# Return whether the date falls on the last day of a commercial bi-week,
|
|
324
|
-
# i.e., on /Sunday/ in a commercial week that is an even-numbered week. From
|
|
325
|
-
# ::Date: "The calendar week is a seven day period within a calendar year,
|
|
326
|
-
# starting on a Monday and identified by its ordinal number within the year;
|
|
327
|
-
# the first calendar week of the year is the one that includes the first
|
|
328
|
-
# Thursday of that year. In the Gregorian calendar, this is equivalent to
|
|
329
|
-
# the week which includes January 4."
|
|
330
|
-
# @return [Boolean]
|
|
331
|
-
def end_of_biweek?
|
|
332
|
-
end_of_biweek == self
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
# :category: Queries
|
|
336
|
-
|
|
337
|
-
# Return whether the date falls on the first day of a commercial week, i.e.,
|
|
338
|
-
# on /Monday/ in a commercial week. From ::Date: "The calendar week is a seven
|
|
339
|
-
# day period within a calendar year, starting on a Monday and identified by
|
|
340
|
-
# its ordinal number within the year; the first calendar week of the year is
|
|
341
|
-
# the one that includes the first Thursday of that year. In the Gregorian
|
|
342
|
-
# calendar, this is equivalent to the week which includes January 4."
|
|
343
|
-
# @return [Boolean]
|
|
344
|
-
def beginning_of_week?
|
|
345
|
-
beginning_of_week == self
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
# :category: Queries
|
|
349
|
-
|
|
350
|
-
# Return whether the date falls on the first day of a commercial week, i.e.,
|
|
351
|
-
# on /Sunday/ in a commercial week. From ::Date: "The calendar week is a seven
|
|
352
|
-
# day period within a calendar year, starting on a Monday and identified by
|
|
353
|
-
# its ordinal number within the year; the first calendar week of the year is
|
|
354
|
-
# the one that includes the first Thursday of that year. In the Gregorian
|
|
355
|
-
# calendar, this is equivalent to the week which includes January 4."
|
|
356
|
-
# @return [Boolean]
|
|
357
|
-
def end_of_week?
|
|
358
|
-
end_of_week == self
|
|
359
|
-
end
|
|
360
|
-
|
|
361
|
-
# Return whether this date falls within a period of *less* than six months
|
|
362
|
-
# from the date `d` using the *Stella v. Graham Page Motors* convention that
|
|
363
|
-
# "less" than six months is true only if this date falls within the range of
|
|
364
|
-
# dates 2 days after date six months before and 2 days before the date six
|
|
365
|
-
# months after the date `d`.
|
|
366
|
-
#
|
|
367
|
-
# @param from_date [::Date] the middle of the six-month range
|
|
368
|
-
# @return [Boolean]
|
|
369
|
-
def within_6mos_of?(from_date)
|
|
370
|
-
from_date = ::Date.parse(from_date) unless from_date.is_a?(Date)
|
|
371
|
-
from_day = from_date.day
|
|
372
|
-
if [28, 29, 30, 31].include?(from_day)
|
|
373
|
-
# Near the end of the month, we need to make sure that when we go
|
|
374
|
-
# forward or backwards 6 months, we do not go past the end of the
|
|
375
|
-
# destination month when finding the "corresponding" day in that
|
|
376
|
-
# month, per Stella v. Graham Page Motors. This refinement was
|
|
377
|
-
# endorsed in the Jammies International case. After we find the
|
|
378
|
-
# corresponding day in the target month, then add two days (for the
|
|
379
|
-
# month six months before the from_date) or subtract two days (for the
|
|
380
|
-
# month six months after the from_date) to get the first and last days
|
|
381
|
-
# of the "within a period of less than six months" date range.
|
|
382
|
-
start_month = from_date.beginning_of_month - 6.months
|
|
383
|
-
start_days = start_month.days_in_month
|
|
384
|
-
start_date = ::Date.new(start_month.year, start_month.month, [start_days, from_day].min) + 2.days
|
|
385
|
-
end_month = from_date.beginning_of_month + 6.months
|
|
386
|
-
end_days = end_month.days_in_month
|
|
387
|
-
end_date = ::Date.new(end_month.year, end_month.month, [end_days, from_day].min) - 2.days
|
|
388
|
-
else
|
|
389
|
-
# ::Date 6 calendar months before self
|
|
390
|
-
start_date = from_date - 6.months + 2.days
|
|
391
|
-
end_date = from_date + 6.months - 2.days
|
|
392
|
-
end
|
|
393
|
-
(start_date..end_date).cover?(self)
|
|
394
|
-
end
|
|
395
|
-
|
|
396
|
-
# Return whether this date is Easter Sunday for the year in which it falls
|
|
397
|
-
# according to the Western Church. A few holidays key off this date as
|
|
398
|
-
# "moveable feasts."
|
|
399
|
-
#
|
|
400
|
-
# @return [Boolean]
|
|
401
|
-
def easter?
|
|
402
|
-
# Am I Easter?
|
|
403
|
-
self == easter_this_year
|
|
404
|
-
end
|
|
405
|
-
|
|
406
|
-
# Return whether this date is the `n`th weekday `wday` of the given `month` in
|
|
407
|
-
# this date's year.
|
|
408
|
-
#
|
|
409
|
-
# @param nth [Integer] number of wday in month, if negative count from end of
|
|
410
|
-
# the month
|
|
411
|
-
# @param wday [Integer] day of week, 0 is Sunday, 1 Monday, etc.
|
|
412
|
-
# @param month [Integer] the month number, 1 is January, 2 is February, etc.
|
|
413
|
-
# @return [Boolean]
|
|
414
|
-
def nth_wday_in_month?(nth, wday, month)
|
|
415
|
-
# Is self the nth weekday in the given month of its year?
|
|
416
|
-
# If nth is negative, count from last day of month
|
|
417
|
-
self == ::Date.nth_wday_in_year_month(nth, wday, year, month)
|
|
418
|
-
end
|
|
419
|
-
|
|
420
|
-
# :category: Relative ::Dates
|
|
421
|
-
# @group Relative ::Dates
|
|
422
|
-
|
|
423
|
-
# Predecessor of self, opposite of `#succ`.
|
|
424
|
-
# @return [::Date]
|
|
425
|
-
def pred
|
|
426
|
-
self - 1.day
|
|
427
|
-
end
|
|
428
|
-
|
|
429
|
-
# NOTE: the ::Date class already has a #succ method.
|
|
430
|
-
|
|
431
|
-
# The date that is the first day of the half-year in which self falls.
|
|
432
|
-
# @return [::Date]
|
|
433
|
-
def beginning_of_half
|
|
434
|
-
if month > 9
|
|
435
|
-
(beginning_of_quarter - 15).beginning_of_quarter
|
|
436
|
-
elsif month > 6
|
|
437
|
-
beginning_of_quarter
|
|
438
|
-
else
|
|
439
|
-
beginning_of_year
|
|
440
|
-
end
|
|
441
|
-
end
|
|
442
|
-
|
|
443
|
-
# :category: Relative ::Dates
|
|
444
|
-
|
|
445
|
-
# The date that is the last day of the half-year in which self falls.
|
|
446
|
-
# @return [::Date]
|
|
447
|
-
def end_of_half
|
|
448
|
-
if month < 4
|
|
449
|
-
(end_of_quarter + 15).end_of_quarter
|
|
450
|
-
elsif month < 7
|
|
451
|
-
end_of_quarter
|
|
452
|
-
else
|
|
453
|
-
end_of_year
|
|
454
|
-
end
|
|
455
|
-
end
|
|
456
|
-
|
|
457
|
-
# :category: Relative ::Dates
|
|
458
|
-
|
|
459
|
-
# The date that is the first day of the bimonth in which self
|
|
460
|
-
# falls. A 'bimonth' is a two-month calendar period beginning on the
|
|
461
|
-
# first day of the odd-numbered months. E.g., 2014-01-01 to
|
|
462
|
-
# 2014-02-28 is the first bimonth of 2014.
|
|
463
|
-
# @return [::Date]
|
|
464
|
-
def beginning_of_bimonth
|
|
465
|
-
if month.odd?
|
|
466
|
-
beginning_of_month
|
|
467
|
-
else
|
|
468
|
-
(self - 1.month).beginning_of_month
|
|
469
|
-
end
|
|
470
|
-
end
|
|
471
|
-
|
|
472
|
-
# :category: Relative ::Dates
|
|
473
|
-
|
|
474
|
-
# The date that is the last day of the bimonth in which self falls.
|
|
475
|
-
# A 'bimonth' is a two-month calendar period beginning on the first
|
|
476
|
-
# day of the odd-numbered months. E.g., 2014-01-01 to 2014-02-28 is
|
|
477
|
-
# the first bimonth of 2014.
|
|
478
|
-
# @return [::Date]
|
|
479
|
-
def end_of_bimonth
|
|
480
|
-
if month.odd?
|
|
481
|
-
(self + 1.month).end_of_month
|
|
482
|
-
else
|
|
483
|
-
end_of_month
|
|
484
|
-
end
|
|
485
|
-
end
|
|
486
|
-
|
|
487
|
-
# :category: Relative ::Dates
|
|
488
|
-
|
|
489
|
-
# The date that is the first day of the semimonth in which self
|
|
490
|
-
# falls. A semimonth is a calendar period beginning on the 1st or
|
|
491
|
-
# 16th of each month and ending on the 15th or last day of the month
|
|
492
|
-
# respectively. So each year has exactly 24 semimonths.
|
|
493
|
-
# @return [::Date]
|
|
494
|
-
def beginning_of_semimonth
|
|
495
|
-
if day >= 16
|
|
496
|
-
::Date.new(year, month, 16)
|
|
497
|
-
else
|
|
498
|
-
beginning_of_month
|
|
499
|
-
end
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
# :category: Relative ::Dates
|
|
503
|
-
|
|
504
|
-
# The date that is the last day of the semimonth in which self
|
|
505
|
-
# falls. A semimonth is a calendar period beginning on the 1st or
|
|
506
|
-
# 16th of each month and ending on the 15th or last day of the month
|
|
507
|
-
# respectively. So each year has exactly 24 semimonths.
|
|
508
|
-
# @return [::Date]
|
|
509
|
-
def end_of_semimonth
|
|
510
|
-
if day <= 15
|
|
511
|
-
::Date.new(year, month, 15)
|
|
512
|
-
else
|
|
513
|
-
end_of_month
|
|
514
|
-
end
|
|
515
|
-
end
|
|
516
|
-
|
|
517
|
-
# :category: Relative ::Dates
|
|
518
|
-
|
|
519
|
-
# In order to accomodate biweeks, we adopt the convention that weeks are
|
|
520
|
-
# numbered so that whatever week contains the year's first
|
|
521
|
-
# Date.beginning_of_week (usually Sunday or Monday) is week number 1.
|
|
522
|
-
# Biweeks are then pairs of weeks: an odd numbered week first, followed by
|
|
523
|
-
# an even numbered week last.
|
|
524
|
-
def week_number
|
|
525
|
-
start_of_weeks = ::Date.new(year, 1, 1)
|
|
526
|
-
bow_wday = DAYSYMS.index(::Date.beginning_of_week)
|
|
527
|
-
start_of_weeks += 1 until start_of_weeks.wday == bow_wday
|
|
528
|
-
if yday >= start_of_weeks.yday
|
|
529
|
-
((yday - start_of_weeks.yday) / 7) + 1
|
|
530
|
-
else
|
|
531
|
-
# One of the days before the start of the year's first week, so it
|
|
532
|
-
# belongs to the last week of the prior year.
|
|
533
|
-
::Date.new(year - 1, 12, 31).week_number
|
|
534
|
-
end
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
# Return the date that is the first day of the biweek in which self
|
|
538
|
-
# falls, or the first day of the year, whichever is later.
|
|
539
|
-
# @return [::Date]
|
|
540
|
-
def beginning_of_biweek
|
|
541
|
-
if week_number.odd?
|
|
542
|
-
beginning_of_week
|
|
543
|
-
else
|
|
544
|
-
(self - 1.week).beginning_of_week
|
|
545
|
-
end
|
|
546
|
-
end
|
|
547
|
-
|
|
548
|
-
# :category: Relative ::Dates
|
|
549
|
-
|
|
550
|
-
# Return the date that is the last day of the biweek in which
|
|
551
|
-
# self falls, or the last day of the year, whichever if earlier.
|
|
552
|
-
# @return [::Date]
|
|
553
|
-
def end_of_biweek
|
|
554
|
-
if week_number.even?
|
|
555
|
-
end_of_week
|
|
556
|
-
else
|
|
557
|
-
(self + 1.week).end_of_week
|
|
558
|
-
end
|
|
559
|
-
end
|
|
560
|
-
|
|
561
|
-
# NOTE: Date#end_of_week and Date#beginning_of_week is defined in ActiveSupport
|
|
562
|
-
|
|
563
|
-
# Return the date that is the first day of the commercial biweek in which
|
|
564
|
-
# self falls. A biweek is a period of two commercial weeks starting with an
|
|
565
|
-
# odd-numbered week and with each week starting in Monday and ending on
|
|
566
|
-
# Sunday.
|
|
567
|
-
# @return [::Date]
|
|
568
|
-
def beginning_of_bicweek
|
|
569
|
-
if cweek.odd?
|
|
570
|
-
beginning_of_week(::Date.beginning_of_week)
|
|
571
|
-
else
|
|
572
|
-
(self - 1.week).beginning_of_week(::Date.beginning_of_week)
|
|
573
|
-
end
|
|
574
|
-
end
|
|
575
|
-
|
|
576
|
-
# :category: Relative ::Dates
|
|
577
|
-
|
|
578
|
-
# Return the date that is the last day of the commercial biweek in which
|
|
579
|
-
# self falls. A biweek is a period of two commercial weeks starting with
|
|
580
|
-
# an odd-numbered week and with each week starting on Monday and ending on
|
|
581
|
-
# Sunday. So this will always return a Sunday in an even-numbered week.
|
|
582
|
-
# In the last week of the year (if it is not part of next year's first
|
|
583
|
-
# week) the end of the biweek will not extend beyond self's week, so that
|
|
584
|
-
# week 1 of the following year will start a new biweek. @return [::Date]
|
|
585
|
-
def end_of_bicweek
|
|
586
|
-
if cweek >= 52 && end_of_week(::Date.beginning_of_week).year > year
|
|
587
|
-
end_of_week(::Date.beginning_of_week)
|
|
588
|
-
elsif cweek.odd?
|
|
589
|
-
(self + 1.week).end_of_week(::Date.beginning_of_week)
|
|
590
|
-
else
|
|
591
|
-
end_of_week(::Date.beginning_of_week)
|
|
592
|
-
end
|
|
593
|
-
end
|
|
594
|
-
|
|
595
|
-
# NOTE: Date#end_of_week and Date#beginning_of_week is defined in ActiveSupport
|
|
596
|
-
|
|
597
|
-
# Return the date that is +n+ calendar halves after this date, where a
|
|
598
|
-
# calendar half is a period of 6 months.
|
|
599
|
-
#
|
|
600
|
-
# @param num [Integer] number of halves to advance, can be negative
|
|
601
|
-
# @return [::Date] new date n halves after this date
|
|
602
|
-
def next_half(num = 1)
|
|
603
|
-
num = num.floor
|
|
604
|
-
return self if num.zero?
|
|
605
|
-
|
|
606
|
-
next_month(num * 6)
|
|
607
|
-
end
|
|
608
|
-
|
|
609
|
-
# Return the date that is +n+ calendar halves before this date, where a
|
|
610
|
-
# calendar half is a period of 6 months.
|
|
611
|
-
#
|
|
612
|
-
# @param num [Integer] number of halves to retreat, can be negative
|
|
613
|
-
# @return [::Date] new date n halves before this date
|
|
614
|
-
def prior_half(num = 1)
|
|
615
|
-
next_half(-num)
|
|
616
|
-
end
|
|
617
|
-
|
|
618
|
-
# Return the date that is +n+ calendar quarters after this date, where a
|
|
619
|
-
# calendar quarter is a period of 3 months.
|
|
620
|
-
#
|
|
621
|
-
# @param num [Integer] number of quarters to advance, can be negative
|
|
622
|
-
# @return [::Date] new date n quarters after this date
|
|
623
|
-
def next_quarter(num = 1)
|
|
624
|
-
num = num.floor
|
|
625
|
-
return self if num.zero?
|
|
626
|
-
|
|
627
|
-
next_month(num * 3)
|
|
628
|
-
end
|
|
629
|
-
|
|
630
|
-
# Return the date that is +n+ calendar quarters before this date, where a
|
|
631
|
-
# calendar quarter is a period of 3 months.
|
|
632
|
-
#
|
|
633
|
-
# @param num [Integer] number of quarters to retreat, can be negative
|
|
634
|
-
# @return [::Date] new date n quarters after this date
|
|
635
|
-
def prior_quarter(num = 1)
|
|
636
|
-
next_quarter(-num)
|
|
637
|
-
end
|
|
638
|
-
|
|
639
|
-
# Return the date that is +n+ calendar bimonths after this date, where a
|
|
640
|
-
# calendar bimonth is a period of 2 months.
|
|
641
|
-
#
|
|
642
|
-
# @param num [Integer] number of bimonths to advance, can be negative
|
|
643
|
-
# @return [::Date] new date n bimonths after this date
|
|
644
|
-
def next_bimonth(num = 1)
|
|
645
|
-
num = num.floor
|
|
646
|
-
return self if num.zero?
|
|
647
|
-
|
|
648
|
-
next_month(num * 2)
|
|
649
|
-
end
|
|
650
|
-
|
|
651
|
-
# Return the date that is +n+ calendar bimonths before this date, where a
|
|
652
|
-
# calendar bimonth is a period of 2 months.
|
|
653
|
-
#
|
|
654
|
-
# @param num [Integer] number of bimonths to retreat, can be negative
|
|
655
|
-
# @return [::Date] new date n bimonths before this date
|
|
656
|
-
def prior_bimonth(num = 1)
|
|
657
|
-
next_bimonth(-num)
|
|
658
|
-
end
|
|
659
|
-
|
|
660
|
-
# Return the date that is +n+ semimonths after this date. Each semimonth begins
|
|
661
|
-
# on the 1st or 16th of the month, and advancing one semimonth from the first
|
|
662
|
-
# half of a month means to go as far past the 16th as the current date is past
|
|
663
|
-
# the 1st; advancing one semimonth from the second half of a month means to go
|
|
664
|
-
# as far into the next month past the 1st as the current date is past the
|
|
665
|
-
# 16th, but never past the 15th of the next month.
|
|
666
|
-
#
|
|
667
|
-
# @param num [Integer] number of semimonths to advance, can be negative
|
|
668
|
-
# @return [::Date] new date n semimonths after this date
|
|
669
|
-
def next_semimonth(num = 1)
|
|
670
|
-
num = num.floor
|
|
671
|
-
return self if num.zero?
|
|
672
|
-
|
|
673
|
-
factor = num.negative? ? -1 : 1
|
|
674
|
-
num = num.abs
|
|
675
|
-
if num.even?
|
|
676
|
-
next_month(num / 2)
|
|
677
|
-
else
|
|
678
|
-
# Advance or retreat one semimonth
|
|
679
|
-
next_sm =
|
|
680
|
-
if day == 1
|
|
681
|
-
if factor.positive?
|
|
682
|
-
beginning_of_month + 16.days
|
|
683
|
-
else
|
|
684
|
-
prior_month.beginning_of_month + 16.days
|
|
685
|
-
end
|
|
686
|
-
elsif day == 16
|
|
687
|
-
if factor.positive?
|
|
688
|
-
next_month.beginning_of_month
|
|
689
|
-
else
|
|
690
|
-
beginning_of_month
|
|
691
|
-
end
|
|
692
|
-
elsif day < 16
|
|
693
|
-
# In the first half of the month (the 2nd to the 15th), go as far past
|
|
694
|
-
# the 16th as the date is past the 1st. Thus, as many as 14 days past
|
|
695
|
-
# the 16th, so at most to the 30th of the month unless there are less
|
|
696
|
-
# than 30 days in the month, then go to the end of the month.
|
|
697
|
-
if factor.positive?
|
|
698
|
-
[beginning_of_month + 16.days + (day - 1).days, end_of_month].min
|
|
699
|
-
else
|
|
700
|
-
[
|
|
701
|
-
prior_month.beginning_of_month + 16.days + (day - 1).days,
|
|
702
|
-
prior_month.end_of_month
|
|
703
|
-
].min
|
|
704
|
-
end
|
|
705
|
-
elsif factor.positive?
|
|
706
|
-
# In the second half of the month (17th to the 31st), go as many
|
|
707
|
-
# days into the next month as we are past the 16th. Thus, as many as
|
|
708
|
-
# 15 days. But we don't want to go past the first half of the next
|
|
709
|
-
# month, so we only go so far as the 15th of the next month.
|
|
710
|
-
# ::Date.parse('2015-02-18').next_semimonth should be the 3rd of the
|
|
711
|
-
# following month.
|
|
712
|
-
next_month.beginning_of_month + [(day - 16), 15].min
|
|
713
|
-
else
|
|
714
|
-
beginning_of_month + [(day - 16), 15].min
|
|
715
|
-
end
|
|
716
|
-
num -= 1
|
|
717
|
-
# Now that n is even, advance (or retreat) n / 2 months unless we're done.
|
|
718
|
-
if num >= 2
|
|
719
|
-
next_sm.next_month(factor * num / 2)
|
|
720
|
-
else
|
|
721
|
-
next_sm
|
|
722
|
-
end
|
|
723
|
-
end
|
|
724
|
-
end
|
|
725
|
-
|
|
726
|
-
# Return the date that is +n+ semimonths before this date. Each semimonth
|
|
727
|
-
# begins on the 1st or 15th of the month, and retreating one semimonth from
|
|
728
|
-
# the first half of a month means to go as far past the 15th of the prior
|
|
729
|
-
# month as the current date is past the 1st; retreating one semimonth from the
|
|
730
|
-
# second half of a month means to go as far past the 1st of the current month
|
|
731
|
-
# as the current date is past the 15th, but never past the 14th of the the
|
|
732
|
-
# current month.
|
|
733
|
-
#
|
|
734
|
-
# @param num [Integer] number of semimonths to retreat, can be negative
|
|
735
|
-
# @return [::Date] new date n semimonths before this date
|
|
736
|
-
def prior_semimonth(num = 1)
|
|
737
|
-
next_semimonth(-num)
|
|
738
|
-
end
|
|
739
|
-
|
|
740
|
-
# Return the date that is +n+ biweeks after this date where each biweek is 14
|
|
741
|
-
# days.
|
|
742
|
-
#
|
|
743
|
-
# @param num [Integer] number of biweeks to advance, can be negative
|
|
744
|
-
# @return [::Date] new date n biweeks after this date
|
|
745
|
-
def next_biweek(num = 1)
|
|
746
|
-
num = num.floor
|
|
747
|
-
return self if num.zero?
|
|
748
|
-
|
|
749
|
-
self + (14 * num)
|
|
750
|
-
end
|
|
751
|
-
|
|
752
|
-
# Return the date that is +n+ biweeks before this date where each biweek is 14
|
|
753
|
-
# days.
|
|
754
|
-
#
|
|
755
|
-
# @param num [Integer] number of biweeks to retreat, can be negative
|
|
756
|
-
# @return [::Date] new date n biweeks before this date
|
|
757
|
-
def prior_biweek(num = 1)
|
|
758
|
-
next_biweek(-num)
|
|
759
|
-
end
|
|
760
|
-
|
|
761
|
-
# Return the date that is +n+ weeks after this date where each week is 7 days.
|
|
762
|
-
# This is different from the #next_week method in active_support, which
|
|
763
|
-
# goes to the first day of the week in the next week and does not take an
|
|
764
|
-
# argument +n+ to go multiple weeks.
|
|
765
|
-
#
|
|
766
|
-
# @param num [Integer] number of weeks to advance
|
|
767
|
-
# @return [::Date] new date n weeks after this date
|
|
768
|
-
def next_week(num = 1)
|
|
769
|
-
num = num.floor
|
|
770
|
-
return self if num.zero?
|
|
771
|
-
|
|
772
|
-
self + (7 * num)
|
|
773
|
-
end
|
|
774
|
-
|
|
775
|
-
# Return the date that is +n+ weeks before this date where each week is 7
|
|
776
|
-
# days.
|
|
777
|
-
#
|
|
778
|
-
# @param num [Integer] number of weeks to retreat
|
|
779
|
-
# @return [::Date] new date n weeks from this date
|
|
780
|
-
def prior_week(num)
|
|
781
|
-
next_week(-num)
|
|
782
|
-
end
|
|
783
|
-
|
|
784
|
-
# NOTE: #next_day is defined in active_support.
|
|
785
|
-
|
|
786
|
-
# Return the date that is +n+ weeks before this date where each week is 7
|
|
787
|
-
# days.
|
|
788
|
-
#
|
|
789
|
-
# @param num [Integer] number of days to retreat
|
|
790
|
-
# @return [::Date] new date n days before this date
|
|
791
|
-
def prior_day(num)
|
|
792
|
-
next_day(-num)
|
|
793
|
-
end
|
|
794
|
-
|
|
795
|
-
# :category: Relative ::Dates
|
|
796
|
-
|
|
797
|
-
# Return the date that is n chunks later than self.
|
|
798
|
-
#
|
|
799
|
-
# @param chunk [Symbol] one of +:year+, +:half+, +:quarter+, +:bimonth+,
|
|
800
|
-
# +:month+, +:semimonth+, +:biweek+, +:week+, or +:day+.
|
|
801
|
-
# @param num [Integer] the number of chunks to add, can be negative
|
|
802
|
-
# @return [::Date] the date n chunks from this date
|
|
803
|
-
def add_chunk(chunk, num = 1)
|
|
804
|
-
case chunk
|
|
805
|
-
when :year
|
|
806
|
-
next_year(num)
|
|
807
|
-
when :half
|
|
808
|
-
next_month(6 * num)
|
|
809
|
-
when :quarter
|
|
810
|
-
next_month(3 * num)
|
|
811
|
-
when :bimonth
|
|
812
|
-
next_month(2 * num)
|
|
813
|
-
when :month
|
|
814
|
-
next_month(num)
|
|
815
|
-
when :semimonth
|
|
816
|
-
next_semimonth(num)
|
|
817
|
-
when :biweek
|
|
818
|
-
next_biweek(num)
|
|
819
|
-
when :week
|
|
820
|
-
next_week(num)
|
|
821
|
-
when :day
|
|
822
|
-
next_day(num)
|
|
823
|
-
else
|
|
824
|
-
raise ArgumentError, "add_chunk unknown chunk: '#{chunk}'"
|
|
825
|
-
end
|
|
826
|
-
end
|
|
827
|
-
|
|
828
|
-
# Return the date that is the beginning of the +chunk+ in which this date
|
|
829
|
-
# falls.
|
|
830
|
-
#
|
|
831
|
-
# @param chunk [Symbol] one of +:year+, +:half+, +:quarter+, +:bimonth+,
|
|
832
|
-
# +:month+, +:semimonth+, +:biweek+, +:week+, or +:day+.
|
|
833
|
-
# @return [::Date] the first date in the chunk-sized period in which this date
|
|
834
|
-
# falls
|
|
835
|
-
def beginning_of_chunk(chunk)
|
|
836
|
-
case chunk
|
|
837
|
-
when :year
|
|
838
|
-
beginning_of_year
|
|
839
|
-
when :half
|
|
840
|
-
beginning_of_half
|
|
841
|
-
when :quarter
|
|
842
|
-
beginning_of_quarter
|
|
843
|
-
when :bimonth
|
|
844
|
-
beginning_of_bimonth
|
|
845
|
-
when :month
|
|
846
|
-
beginning_of_month
|
|
847
|
-
when :semimonth
|
|
848
|
-
beginning_of_semimonth
|
|
849
|
-
when :biweek
|
|
850
|
-
beginning_of_biweek
|
|
851
|
-
when :week
|
|
852
|
-
beginning_of_week
|
|
853
|
-
when :day
|
|
854
|
-
self
|
|
855
|
-
else
|
|
856
|
-
raise ArgumentError, "unknown chunk sym: '#{chunk}'"
|
|
857
|
-
end
|
|
858
|
-
end
|
|
859
|
-
|
|
860
|
-
# Return the date that is the end of the +chunk+ in which this date
|
|
861
|
-
# falls.
|
|
862
|
-
#
|
|
863
|
-
# @param chunk [Symbol] one of +:year+, +:half+, +:quarter+, +:bimonth+,
|
|
864
|
-
# +:month+, +:semimonth+, +:biweek+, +:week+, or +:day+.
|
|
865
|
-
# @return [::Date] the first date in the chunk-sized period in which this date
|
|
866
|
-
# falls
|
|
867
|
-
def end_of_chunk(chunk)
|
|
868
|
-
case chunk
|
|
869
|
-
when :year
|
|
870
|
-
end_of_year
|
|
871
|
-
when :half
|
|
872
|
-
end_of_half
|
|
873
|
-
when :quarter
|
|
874
|
-
end_of_quarter
|
|
875
|
-
when :bimonth
|
|
876
|
-
end_of_bimonth
|
|
877
|
-
when :month
|
|
878
|
-
end_of_month
|
|
879
|
-
when :semimonth
|
|
880
|
-
end_of_semimonth
|
|
881
|
-
when :biweek
|
|
882
|
-
end_of_biweek
|
|
883
|
-
when :week
|
|
884
|
-
end_of_week
|
|
885
|
-
when :day
|
|
886
|
-
self
|
|
887
|
-
else
|
|
888
|
-
raise ArgumentError, "unknown chunk: '#{chunk}'"
|
|
889
|
-
end
|
|
890
|
-
end
|
|
891
|
-
|
|
892
|
-
# Return whether the date that is the beginning of the +chunk+
|
|
893
|
-
#
|
|
894
|
-
# @param chunk [Symbol] one of +:year+, +:half+, +:quarter+, +:bimonth+,
|
|
895
|
-
# +:month+, +:semimonth+, +:biweek+, +:week+, or +:day+.
|
|
896
|
-
# @return [::Boolean] whether this date begins a chunk
|
|
897
|
-
def beginning_of_chunk?(chunk)
|
|
898
|
-
self == beginning_of_chunk(chunk)
|
|
899
|
-
end
|
|
900
|
-
|
|
901
|
-
# Return whether the date that is the end of the +chunk+
|
|
902
|
-
#
|
|
903
|
-
# @param chunk [Symbol] one of +:year+, +:half+, +:quarter+, +:bimonth+,
|
|
904
|
-
# +:month+, +:semimonth+, +:biweek+, +:week+, or +:day+.
|
|
905
|
-
# @return [::Boolean] whether this date ends a chunk
|
|
906
|
-
def end_of_chunk?(chunk)
|
|
907
|
-
self == end_of_chunk(chunk)
|
|
908
|
-
end
|
|
909
|
-
|
|
910
|
-
# @group Holidays and Workdays
|
|
911
|
-
|
|
912
|
-
# Does self fall on a weekend?
|
|
913
|
-
# @return [Boolean]
|
|
914
|
-
def weekend?
|
|
915
|
-
saturday? || sunday?
|
|
916
|
-
end
|
|
917
|
-
|
|
918
|
-
# :category: Queries
|
|
919
|
-
|
|
920
|
-
# Does self fall on a weekday?
|
|
921
|
-
# @return [Boolean]
|
|
922
|
-
def weekday?
|
|
923
|
-
!weekend?
|
|
924
|
-
end
|
|
925
|
-
|
|
926
|
-
# Return the date for Easter in the Western Church for the year in which this
|
|
927
|
-
# date falls.
|
|
928
|
-
#
|
|
929
|
-
# @return [::Date]
|
|
930
|
-
def easter_this_year
|
|
931
|
-
# Return the date of Easter in self's year
|
|
932
|
-
::Date.easter(year)
|
|
933
|
-
end
|
|
934
|
-
|
|
935
|
-
# Holidays decreed by Presidential proclamation
|
|
936
|
-
FED_DECREED_HOLIDAYS =
|
|
937
|
-
[
|
|
938
|
-
# Obama decree extra day before Christmas See
|
|
939
|
-
# http://www.whitehouse.gov/the-press-office/2012/12/21
|
|
940
|
-
::Date.parse('2012-12-24'),
|
|
941
|
-
# And Trump
|
|
942
|
-
::Date.parse('2018-12-24'),
|
|
943
|
-
::Date.parse('2019-12-24'),
|
|
944
|
-
::Date.parse('2020-12-24'),
|
|
945
|
-
# Biden
|
|
946
|
-
::Date.parse('2024-12-24'),
|
|
947
|
-
]
|
|
948
|
-
|
|
949
|
-
# Presidential funeral since JFK
|
|
950
|
-
PRESIDENTIAL_FUNERALS = [
|
|
951
|
-
# JKF Funeral
|
|
952
|
-
::Date.parse('1963-11-25'),
|
|
953
|
-
# DWE Funeral
|
|
954
|
-
::Date.parse('1969-03-31'),
|
|
955
|
-
# HST Funeral
|
|
956
|
-
::Date.parse('1972-12-28'),
|
|
957
|
-
# LBJ Funeral
|
|
958
|
-
::Date.parse('1973-01-25'),
|
|
959
|
-
# RMN Funeral
|
|
960
|
-
::Date.parse('1994-04-27'),
|
|
961
|
-
# RWR Funeral
|
|
962
|
-
::Date.parse('2004-06-11'),
|
|
963
|
-
# GTF Funeral
|
|
964
|
-
::Date.parse('2007-01-02'),
|
|
965
|
-
# GHWBFuneral
|
|
966
|
-
::Date.parse('2018-12-05'),
|
|
967
|
-
# JEC Funeral
|
|
968
|
-
::Date.parse('2025-01-09'),
|
|
969
|
-
]
|
|
970
|
-
|
|
971
|
-
# Return whether this date is a United States federal holiday.
|
|
972
|
-
#
|
|
973
|
-
# Calculations for Federal holidays are based on 5 USC 6103, include all
|
|
974
|
-
# weekends, Presidential funerals, and holidays decreed executive orders.
|
|
975
|
-
#
|
|
976
|
-
# @return [Boolean]
|
|
977
|
-
def fed_holiday?
|
|
978
|
-
# All Saturdays and Sundays are "holidays"
|
|
979
|
-
return true if weekend?
|
|
980
|
-
|
|
981
|
-
# Some days are holidays by executive decree
|
|
982
|
-
return true if FED_DECREED_HOLIDAYS.include?(self)
|
|
983
|
-
|
|
984
|
-
# Presidential funerals
|
|
985
|
-
return true if PRESIDENTIAL_FUNERALS.include?(self)
|
|
986
|
-
|
|
987
|
-
# Is self a fixed holiday
|
|
988
|
-
return true if fed_fixed_holiday? || fed_moveable_feast?
|
|
989
|
-
|
|
990
|
-
if friday? && month == 12 && day == 26
|
|
991
|
-
# If Christmas falls on a Thursday, apparently, the Friday after is
|
|
992
|
-
# treated as a holiday as well. See 2003, 2008, for example.
|
|
993
|
-
true
|
|
994
|
-
elsif friday?
|
|
995
|
-
# A Friday is a holiday if a fixed-date holiday
|
|
996
|
-
# would fall on the following Saturday
|
|
997
|
-
(self + 1).fed_fixed_holiday? || (self + 1).fed_moveable_feast?
|
|
998
|
-
elsif monday?
|
|
999
|
-
# A Monday is a holiday if a fixed-date holiday
|
|
1000
|
-
# would fall on the preceding Sunday
|
|
1001
|
-
(self - 1).fed_fixed_holiday? || (self - 1).fed_moveable_feast?
|
|
1002
|
-
elsif (year % 4 == 1) && year > 1965 && mon == 1 && mday == 20
|
|
1003
|
-
# Inauguration Day after 1965 is a federal holiday, but if it falls on a
|
|
1004
|
-
# Sunday, the following Monday is observed, but if it falls on a
|
|
1005
|
-
# Saturday, the prior Friday is /not/ observed. So, we can't just count
|
|
1006
|
-
# this as a regular fixed holiday.
|
|
1007
|
-
true
|
|
1008
|
-
elsif monday? && (year % 4 == 1) && year > 1965 && mon == 1 && mday == 21
|
|
1009
|
-
# Inauguration Day after 1965 is a federal holiday, but if it falls on a
|
|
1010
|
-
# Sunday, the following Monday is observed, but if it falls on a
|
|
1011
|
-
# Saturday, the prior Friday is /not/ observed.
|
|
1012
|
-
true
|
|
1013
|
-
else
|
|
1014
|
-
false
|
|
1015
|
-
end
|
|
1016
|
-
end
|
|
1017
|
-
|
|
1018
|
-
# Return whether this date is a date on which the US federal government is
|
|
1019
|
-
# open for business. It is the opposite of #fed_holiday?
|
|
1020
|
-
#
|
|
1021
|
-
# @return [Boolean]
|
|
1022
|
-
def fed_workday?
|
|
1023
|
-
!fed_holiday?
|
|
1024
|
-
end
|
|
1025
|
-
|
|
1026
|
-
# :category: Queries
|
|
1027
|
-
|
|
1028
|
-
# Return the date that is n federal workdays after or before (if n < 0) this
|
|
1029
|
-
# date.
|
|
1030
|
-
#
|
|
1031
|
-
# @param num [Integer] number of federal workdays to add to this date
|
|
1032
|
-
# @return [::Date]
|
|
1033
|
-
def add_fed_workdays(num)
|
|
1034
|
-
d = dup
|
|
1035
|
-
return d if num.zero?
|
|
1036
|
-
|
|
1037
|
-
incr = num.negative? ? -1 : 1
|
|
1038
|
-
num = num.abs
|
|
1039
|
-
while num.positive?
|
|
1040
|
-
d += incr
|
|
1041
|
-
num -= 1 if d.fed_workday?
|
|
1042
|
-
end
|
|
1043
|
-
d
|
|
1044
|
-
end
|
|
1045
|
-
|
|
1046
|
-
# Return the next federal workday after this date. The date returned is always
|
|
1047
|
-
# a date at least one day after this date, never this date.
|
|
1048
|
-
#
|
|
1049
|
-
# @return [::Date]
|
|
1050
|
-
def next_fed_workday
|
|
1051
|
-
add_fed_workdays(1)
|
|
1052
|
-
end
|
|
1053
|
-
|
|
1054
|
-
# Return the last federal workday before this date. The date returned is always
|
|
1055
|
-
# a date at least one day before this date, never this date.
|
|
1056
|
-
#
|
|
1057
|
-
# @return [::Date]
|
|
1058
|
-
def prior_fed_workday
|
|
1059
|
-
add_fed_workdays(-1)
|
|
1060
|
-
end
|
|
1061
|
-
|
|
1062
|
-
# Return this date if its a federal workday, otherwise skip forward to the
|
|
1063
|
-
# first later federal workday.
|
|
1064
|
-
#
|
|
1065
|
-
# @return [::Date]
|
|
1066
|
-
def next_until_fed_workday
|
|
1067
|
-
date = dup
|
|
1068
|
-
date += 1 until date.fed_workday?
|
|
1069
|
-
date
|
|
1070
|
-
end
|
|
1071
|
-
|
|
1072
|
-
# Return this if its a federal workday, otherwise skip back to the first prior
|
|
1073
|
-
# federal workday.
|
|
1074
|
-
def prior_until_fed_workday
|
|
1075
|
-
date = dup
|
|
1076
|
-
date -= 1 until date.fed_workday?
|
|
1077
|
-
date
|
|
1078
|
-
end
|
|
1079
|
-
|
|
1080
|
-
protected
|
|
1081
|
-
|
|
1082
|
-
def fed_fixed_holiday?
|
|
1083
|
-
# Fixed-date holidays on weekdays
|
|
1084
|
-
if mon == 1 && mday == 1
|
|
1085
|
-
# New Years (January 1),
|
|
1086
|
-
true
|
|
1087
|
-
elsif mon == 6 && mday == 19 && year >= 2021
|
|
1088
|
-
# Juneteenth,
|
|
1089
|
-
true
|
|
1090
|
-
elsif mon == 7 && mday == 4
|
|
1091
|
-
# Independence Day (July 4),
|
|
1092
|
-
true
|
|
1093
|
-
elsif mon == 11 && mday == 11
|
|
1094
|
-
# Veterans Day (November 11),
|
|
1095
|
-
true
|
|
1096
|
-
elsif mon == 12 && mday == 25
|
|
1097
|
-
# Christmas (December 25), and
|
|
1098
|
-
true
|
|
1099
|
-
else
|
|
1100
|
-
false
|
|
1101
|
-
end
|
|
1102
|
-
end
|
|
1103
|
-
|
|
1104
|
-
def fed_moveable_feast?
|
|
1105
|
-
# See if today is a "movable feast," all of which are
|
|
1106
|
-
# rigged to fall on Monday except Thanksgiving
|
|
1107
|
-
|
|
1108
|
-
# No moveable feasts in certain months
|
|
1109
|
-
if [3, 4, 6, 7, 8, 12].include?(month)
|
|
1110
|
-
false
|
|
1111
|
-
elsif monday?
|
|
1112
|
-
moveable_mondays = []
|
|
1113
|
-
# MLK's Birthday (Third Monday in Jan)
|
|
1114
|
-
moveable_mondays << nth_wday_in_month?(3, 1, 1)
|
|
1115
|
-
# Washington's Birthday (Third Monday in Feb)
|
|
1116
|
-
moveable_mondays << nth_wday_in_month?(3, 1, 2)
|
|
1117
|
-
# Memorial Day (Last Monday in May)
|
|
1118
|
-
moveable_mondays << nth_wday_in_month?(-1, 1, 5)
|
|
1119
|
-
# Labor Day (First Monday in Sep)
|
|
1120
|
-
moveable_mondays << nth_wday_in_month?(1, 1, 9)
|
|
1121
|
-
# Columbus Day (Second Monday in Oct)
|
|
1122
|
-
moveable_mondays << nth_wday_in_month?(2, 1, 10)
|
|
1123
|
-
# Other Mondays
|
|
1124
|
-
moveable_mondays.any?
|
|
1125
|
-
elsif thursday?
|
|
1126
|
-
# Thanksgiving Day (Fourth Thur in Nov)
|
|
1127
|
-
nth_wday_in_month?(4, 4, 11)
|
|
1128
|
-
else
|
|
1129
|
-
false
|
|
1130
|
-
end
|
|
1131
|
-
end
|
|
1132
|
-
|
|
1133
|
-
# @group NYSE Holidays and Workdays
|
|
1134
|
-
|
|
1135
|
-
# :category: Queries
|
|
1136
|
-
|
|
1137
|
-
public
|
|
1138
|
-
|
|
1139
|
-
# Returns whether this date is one on which the NYSE was or is expected to be
|
|
1140
|
-
# closed for business.
|
|
1141
|
-
#
|
|
1142
|
-
# Calculations for NYSE holidays are from Rule 51 and supplementary materials
|
|
1143
|
-
# for the Rules of the New York Stock Exchange, Inc.
|
|
1144
|
-
#
|
|
1145
|
-
# * General Rule 1: if a regular holiday falls on Saturday, observe it on
|
|
1146
|
-
# the preceding Friday.
|
|
1147
|
-
# * General Rule 2: if a regular holiday falls on Sunday, observe it on
|
|
1148
|
-
# the following Monday.
|
|
1149
|
-
#
|
|
1150
|
-
# These are the regular holidays:
|
|
1151
|
-
#
|
|
1152
|
-
# * New Year's Day, January 1.
|
|
1153
|
-
# * Birthday of Martin Luther King, Jr., the third Monday in January.
|
|
1154
|
-
# * Washington's Birthday, the third Monday in February.
|
|
1155
|
-
# * Good Friday Friday before Easter Sunday. NOTE: this is not a fed holiday
|
|
1156
|
-
# * Memorial Day, the last Monday in May.
|
|
1157
|
-
# * Independence Day, July 4.
|
|
1158
|
-
# * Labor Day, the first Monday in September.
|
|
1159
|
-
# * Thanksgiving Day, the fourth Thursday in November.
|
|
1160
|
-
# * Christmas Day, December 25.
|
|
1161
|
-
#
|
|
1162
|
-
# Columbus and Veterans days not observed.
|
|
1163
|
-
#
|
|
1164
|
-
# In addition, there have been several days on which the exchange has been
|
|
1165
|
-
# closed for special events such as Presidential funerals, the 9-11 attacks,
|
|
1166
|
-
# the paper-work crisis in the 1960's, hurricanes, etc. All of these are
|
|
1167
|
-
# considered holidays for purposes of this method.
|
|
1168
|
-
#
|
|
1169
|
-
# In addition, every weekend is considered a holiday.
|
|
1170
|
-
#
|
|
1171
|
-
# @return [Boolean]
|
|
1172
|
-
def nyse_holiday?
|
|
1173
|
-
# All Saturdays and Sundays are "holidays"
|
|
1174
|
-
return true if weekend?
|
|
1175
|
-
|
|
1176
|
-
# Presidential funerals, observed by NYSE as well.
|
|
1177
|
-
return true if PRESIDENTIAL_FUNERALS.include?(self)
|
|
1178
|
-
|
|
1179
|
-
# Is self a fixed holiday
|
|
1180
|
-
return true if nyse_fixed_holiday? || nyse_moveable_feast?
|
|
1181
|
-
|
|
1182
|
-
return true if nyse_special_holiday?
|
|
1183
|
-
|
|
1184
|
-
if friday? && (self >= ::Date.parse('1959-07-03'))
|
|
1185
|
-
# A Friday is a holiday if a holiday would fall on the following
|
|
1186
|
-
# Saturday. The rule does not apply if the Friday "ends a monthly or
|
|
1187
|
-
# yearly accounting period." Adopted July 3, 1959. E.g, December 31,
|
|
1188
|
-
# 2010, fell on a Friday, so New Years was on Saturday, but the NYSE
|
|
1189
|
-
# opened because it ended a yearly accounting period. I believe 12/31
|
|
1190
|
-
# is the only date to which the exception can apply since only New
|
|
1191
|
-
# Year's can fall on the first of the month.
|
|
1192
|
-
!end_of_quarter? &&
|
|
1193
|
-
((self + 1).nyse_fixed_holiday? || (self + 1).nyse_moveable_feast?)
|
|
1194
|
-
elsif monday?
|
|
1195
|
-
# A Monday is a holiday if a holiday would fall on the
|
|
1196
|
-
# preceding Sunday. This has apparently always been the rule.
|
|
1197
|
-
(self - 1).nyse_fixed_holiday? || (self - 1).nyse_moveable_feast?
|
|
1198
|
-
else
|
|
1199
|
-
false
|
|
1200
|
-
end
|
|
1201
|
-
end
|
|
1202
|
-
|
|
1203
|
-
# Return whether the NYSE is open for trading on this date.
|
|
1204
|
-
#
|
|
1205
|
-
# @return [Boolean]
|
|
1206
|
-
def nyse_workday?
|
|
1207
|
-
!nyse_holiday?
|
|
1208
|
-
end
|
|
1209
|
-
alias_method :trading_day?, :nyse_workday?
|
|
1210
|
-
|
|
1211
|
-
# Return a new date that is n NYSE trading days after or before (if n < 0)
|
|
1212
|
-
# this date.
|
|
1213
|
-
#
|
|
1214
|
-
# @param num [Integer] number of NYSE trading days to add to this date
|
|
1215
|
-
# @return [::Date]
|
|
1216
|
-
def add_nyse_workdays(num)
|
|
1217
|
-
d = dup
|
|
1218
|
-
return d if num.zero?
|
|
1219
|
-
|
|
1220
|
-
incr = num.negative? ? -1 : 1
|
|
1221
|
-
num = num.abs
|
|
1222
|
-
while num.positive?
|
|
1223
|
-
d += incr
|
|
1224
|
-
num -= 1 if d.nyse_workday?
|
|
1225
|
-
end
|
|
1226
|
-
d
|
|
1227
|
-
end
|
|
1228
|
-
alias_method :add_trading_days, :add_nyse_workdays
|
|
1229
|
-
|
|
1230
|
-
# Return the next NYSE trading day after this date. The date returned is always
|
|
1231
|
-
# a date at least one day after this date, never this date.
|
|
1232
|
-
#
|
|
1233
|
-
# @return [::Date]
|
|
1234
|
-
def next_nyse_workday
|
|
1235
|
-
add_nyse_workdays(1)
|
|
1236
|
-
end
|
|
1237
|
-
alias_method :next_trading_day, :next_nyse_workday
|
|
1238
|
-
|
|
1239
|
-
# Return the last NYSE trading day before this date. The date returned is always
|
|
1240
|
-
# a date at least one day before this date, never this date.
|
|
1241
|
-
#
|
|
1242
|
-
# @return [::Date]
|
|
1243
|
-
def prior_nyse_workday
|
|
1244
|
-
add_nyse_workdays(-1)
|
|
1245
|
-
end
|
|
1246
|
-
alias_method :prior_trading_day, :prior_nyse_workday
|
|
1247
|
-
|
|
1248
|
-
# Return this date if its a trading day, otherwise skip forward to the first
|
|
1249
|
-
# later trading day.
|
|
1250
|
-
#
|
|
1251
|
-
# @return [::Date]
|
|
1252
|
-
def next_until_nyse_workday
|
|
1253
|
-
date = dup
|
|
1254
|
-
date += 1 until date.trading_day?
|
|
1255
|
-
date
|
|
1256
|
-
end
|
|
1257
|
-
alias_method :next_until_trading_day, :next_until_nyse_workday
|
|
1258
|
-
|
|
1259
|
-
# Return this date if its a trading day, otherwise skip back to the first prior
|
|
1260
|
-
# trading day.
|
|
1261
|
-
#
|
|
1262
|
-
# @return [::Date]
|
|
1263
|
-
def prior_until_trading_day
|
|
1264
|
-
date = dup
|
|
1265
|
-
date -= 1 until date.trading_day?
|
|
1266
|
-
date
|
|
1267
|
-
end
|
|
1268
|
-
|
|
1269
|
-
# Return whether this date is a fixed holiday for the NYSE, that is, a holiday
|
|
1270
|
-
# that falls on the same date each year.
|
|
1271
|
-
#
|
|
1272
|
-
# @return [Boolean]
|
|
1273
|
-
def nyse_fixed_holiday?
|
|
1274
|
-
# Fixed-date holidays
|
|
1275
|
-
if mon == 1 && mday == 1
|
|
1276
|
-
# New Years (January 1),
|
|
1277
|
-
true
|
|
1278
|
-
elsif mon == 7 && mday == 4
|
|
1279
|
-
# Independence Day (July 4),
|
|
1280
|
-
true
|
|
1281
|
-
elsif mon == 12 && mday == 25
|
|
1282
|
-
# Christmas (December 25), and
|
|
1283
|
-
true
|
|
1284
|
-
else
|
|
1285
|
-
false
|
|
1286
|
-
end
|
|
1287
|
-
end
|
|
1288
|
-
|
|
1289
|
-
# :category: Queries
|
|
1290
|
-
|
|
1291
|
-
# Return whether this date is a non-fixed holiday for the NYSE, that is, a holiday
|
|
1292
|
-
# that can fall on different dates each year, a so-called "moveable feast".
|
|
1293
|
-
#
|
|
1294
|
-
# @return [Boolean]
|
|
1295
|
-
def nyse_moveable_feast?
|
|
1296
|
-
# See if today is a "movable feast," all of which are
|
|
1297
|
-
# rigged to fall on Monday except Thanksgiving
|
|
1298
|
-
|
|
1299
|
-
# No moveable feasts in certain months
|
|
1300
|
-
return false if [6, 7, 8, 10, 12].include?(month)
|
|
1301
|
-
|
|
1302
|
-
case month
|
|
1303
|
-
when 1
|
|
1304
|
-
# MLK's Birthday (Third Monday in Jan) since 1998
|
|
1305
|
-
year >= 1998 && nth_wday_in_month?(3, 1, 1)
|
|
1306
|
-
when 2
|
|
1307
|
-
# Washington's Birthday was celebrated on February 22 until 1970. In
|
|
1308
|
-
# 1971 and later, it was moved to the third Monday in February. Note:
|
|
1309
|
-
# Lincoln's birthday is not an official holiday, but is sometimes
|
|
1310
|
-
# included with Washington's and called "Presidents' Day."
|
|
1311
|
-
if year <= 1970
|
|
1312
|
-
month == 2 && day == 22
|
|
1313
|
-
else
|
|
1314
|
-
nth_wday_in_month?(3, 1, 2)
|
|
1315
|
-
end
|
|
1316
|
-
when 3, 4
|
|
1317
|
-
# Good Friday
|
|
1318
|
-
if !friday?
|
|
1319
|
-
false
|
|
1320
|
-
elsif [1898, 1906, 1907].include?(year)
|
|
1321
|
-
# Good Friday, the Friday before Easter, except certain years
|
|
1322
|
-
false
|
|
1323
|
-
else
|
|
1324
|
-
(self + 2).easter?
|
|
1325
|
-
end
|
|
1326
|
-
when 5
|
|
1327
|
-
# Memorial Day (Last Monday in May)
|
|
1328
|
-
year <= 1970 ? (month == 5 && day == 30) : nth_wday_in_month?(-1, 1, 5)
|
|
1329
|
-
when 9
|
|
1330
|
-
# Labor Day (First Monday in Sep)
|
|
1331
|
-
nth_wday_in_month?(1, 1, 9)
|
|
1332
|
-
when 10
|
|
1333
|
-
# Columbus Day (Oct 12) 1909--1953
|
|
1334
|
-
year.between?(1909, 1953) && day == 12
|
|
1335
|
-
when 11
|
|
1336
|
-
if tuesday?
|
|
1337
|
-
# Election Day. Until 1968 all Election Days. From 1972 to 1980
|
|
1338
|
-
# Election Day in presidential years only. Election Day is the first
|
|
1339
|
-
# Tuesday after the first Monday in November.
|
|
1340
|
-
first_tuesday = ::Date.nth_wday_in_year_month(1, 1, year, 11) + 1
|
|
1341
|
-
is_election_day = (self == first_tuesday)
|
|
1342
|
-
if year <= 1968
|
|
1343
|
-
is_election_day
|
|
1344
|
-
elsif year <= 1980
|
|
1345
|
-
is_election_day && (year % 4).zero?
|
|
1346
|
-
else
|
|
1347
|
-
false
|
|
1348
|
-
end
|
|
1349
|
-
elsif thursday?
|
|
1350
|
-
# Historically Thanksgiving (NYSE closed all day) had been declared to
|
|
1351
|
-
# be the last Thursday in November until 1938; the next-to-last
|
|
1352
|
-
# Thursday in November from 1939 to 1941 (therefore the 3rd Thursday
|
|
1353
|
-
# in 1940 and 1941); the last Thursday in November in 1942; the fourth
|
|
1354
|
-
# Thursday in November since 1943;
|
|
1355
|
-
if year < 1938
|
|
1356
|
-
nth_wday_in_month?(-1, 4, 11)
|
|
1357
|
-
elsif year <= 1941
|
|
1358
|
-
nth_wday_in_month?(3, 4, 11)
|
|
1359
|
-
elsif year == 1942
|
|
1360
|
-
nth_wday_in_month?(-1, 4, 11)
|
|
1361
|
-
else
|
|
1362
|
-
nth_wday_in_month?(4, 4, 11)
|
|
1363
|
-
end
|
|
1364
|
-
elsif day == 11
|
|
1365
|
-
# Armistice or Veterans Day. 1918--1921; 1934--1953.
|
|
1366
|
-
year.between?(1918, 1921) || year.between?(1934, 1953)
|
|
1367
|
-
else
|
|
1368
|
-
false
|
|
1369
|
-
end
|
|
1370
|
-
else
|
|
1371
|
-
false
|
|
1372
|
-
end
|
|
1373
|
-
end
|
|
1374
|
-
|
|
1375
|
-
# :category: Queries
|
|
1376
|
-
|
|
1377
|
-
# They NYSE has closed on several occasions outside its normal holiday
|
|
1378
|
-
# rules. This detects those dates beginning in 1960. Closing for part of a
|
|
1379
|
-
# day is not counted. See http://www1.nyse.com/pdfs/closings.pdf. Return
|
|
1380
|
-
# whether this date is one of those special closings.
|
|
1381
|
-
#
|
|
1382
|
-
# @return [Boolean]
|
|
1383
|
-
def nyse_special_holiday?
|
|
1384
|
-
return false if self <= ::Date.parse('1960-01-01')
|
|
1385
|
-
|
|
1386
|
-
return true if PRESIDENTIAL_FUNERALS.include?(self)
|
|
1387
|
-
|
|
1388
|
-
case self
|
|
1389
|
-
when ::Date.parse('1961-05-29')
|
|
1390
|
-
# Day before Decoaration Day
|
|
1391
|
-
true
|
|
1392
|
-
when ::Date.parse('1963-11-25')
|
|
1393
|
-
# President Kennedy's funeral
|
|
1394
|
-
true
|
|
1395
|
-
when ::Date.parse('1965-12-24')
|
|
1396
|
-
# Christmas eve unscheduled for normal holiday
|
|
1397
|
-
true
|
|
1398
|
-
when ::Date.parse('1968-02-12')
|
|
1399
|
-
# Lincoln birthday
|
|
1400
|
-
true
|
|
1401
|
-
when ::Date.parse('1968-04-09')
|
|
1402
|
-
# Mourning MLK
|
|
1403
|
-
true
|
|
1404
|
-
when ::Date.parse('1968-07-05')
|
|
1405
|
-
# Day after Independence Day
|
|
1406
|
-
true
|
|
1407
|
-
when (::Date.parse('1968-06-12')..::Date.parse('1968-12-31'))
|
|
1408
|
-
# Paperwork crisis (closed on Wednesdays if no other holiday in week)
|
|
1409
|
-
wednesday? && (self - 2).nyse_workday? && (self - 1).nyse_workday? &&
|
|
1410
|
-
(self + 1).nyse_workday? && (self + 2).nyse_workday?
|
|
1411
|
-
when ::Date.parse('1969-02-10')
|
|
1412
|
-
# Heavy snow
|
|
1413
|
-
true
|
|
1414
|
-
when ::Date.parse('1969-07-21')
|
|
1415
|
-
# Moon landing
|
|
1416
|
-
true
|
|
1417
|
-
when ::Date.parse('1977-07-14')
|
|
1418
|
-
# Electrical blackout NYC
|
|
1419
|
-
true
|
|
1420
|
-
when ::Date.parse('1985-09-27')
|
|
1421
|
-
# Hurricane Gloria
|
|
1422
|
-
true
|
|
1423
|
-
when (::Date.parse('2001-09-11')..::Date.parse('2001-09-14'))
|
|
1424
|
-
# 9-11 Attacks
|
|
1425
|
-
true
|
|
1426
|
-
when ::Date.parse('2007-01-02')
|
|
1427
|
-
# Observance death of President Ford
|
|
1428
|
-
true
|
|
1429
|
-
when ::Date.parse('2012-10-29'), ::Date.parse('2012-10-30')
|
|
1430
|
-
# Hurricane Sandy
|
|
1431
|
-
true
|
|
1432
|
-
else
|
|
1433
|
-
false
|
|
1434
|
-
end
|
|
1435
|
-
end
|
|
1436
|
-
|
|
1437
|
-
module ClassMethods
|
|
1438
|
-
# @group Parsing
|
|
1439
|
-
#
|
|
1440
|
-
# Convert a string +str+ with an American style date into a ::Date object
|
|
1441
|
-
#
|
|
1442
|
-
# An American style date is of the form `MM/DD/YYYY`, that is it places the
|
|
1443
|
-
# month first, then the day of the month, and finally the year. The European
|
|
1444
|
-
# convention is typically to place the day of the month first, `DD/MM/YYYY`.
|
|
1445
|
-
# A date found in the wild can be ambiguous, e.g. 3/5/2014, but a date
|
|
1446
|
-
# string known to be using the American convention can be parsed using this
|
|
1447
|
-
# method. Both the month and the day can be a single digit. The year can be
|
|
1448
|
-
# either 2 or 4 digits, and if given as 2 digits, it adds 2000 to it to give
|
|
1449
|
-
# the year.
|
|
1450
|
-
#
|
|
1451
|
-
# @example
|
|
1452
|
-
# ::Date.parse_american('9/11/2001') #=> ::Date(2011, 9, 11)
|
|
1453
|
-
# ::Date.parse_american('9/11/01') #=> ::Date(2011, 9, 11)
|
|
1454
|
-
# ::Date.parse_american('9/11/1') #=> ArgumentError
|
|
1455
|
-
#
|
|
1456
|
-
# @param str [String, #to_s] a stringling of the form MM/DD/YYYY
|
|
1457
|
-
# @return [::Date] the date represented by the str paramenter.
|
|
1458
|
-
def parse_american(str)
|
|
1459
|
-
re = %r{\A\s*(\d\d?)\s*[-/]\s*(\d\d?)\s*[-/]\s*((\d\d)?\d\d)\s*\z}
|
|
1460
|
-
unless str.to_s =~ re
|
|
1461
|
-
raise ArgumentError, "date string must be of form 'MM?/DD?/YY(YY)?'"
|
|
1462
|
-
end
|
|
1463
|
-
|
|
1464
|
-
year = $3.to_i
|
|
1465
|
-
month = $1.to_i
|
|
1466
|
-
day = $2.to_i
|
|
1467
|
-
year += 2000 if year < 100
|
|
1468
|
-
::Date.new(year, month, day)
|
|
1469
|
-
end
|
|
1470
|
-
|
|
1471
|
-
# Convert a 'period spec' `spec` to a ::Date. A date spec is a short-hand way of
|
|
1472
|
-
# specifying a calendar period either absolutely or relative to the computer
|
|
1473
|
-
# clock. This method returns the first date of that period, when `spec_type`
|
|
1474
|
-
# is set to `:from`, the default, and returns the last date of the period
|
|
1475
|
-
# when `spec_type` is `:to`.
|
|
1476
|
-
#
|
|
1477
|
-
# There are a number of forms the `spec` can take. In each case,
|
|
1478
|
-
# `::Date.parse_spec` returns the first date in the period if `spec_type` is
|
|
1479
|
-
# `:from` and the last date in the period if `spec_type` is `:to`:
|
|
1480
|
-
#
|
|
1481
|
-
# * `YYYY-MM-DD` a particular date, so `:from` and `:to` return the same
|
|
1482
|
-
# * `YYYY` is the whole year `YYYY`,
|
|
1483
|
-
# * `YYYY-1H` or `YYYY-H1` is the first calendar half in year `YYYY`,
|
|
1484
|
-
# * `H2` or `2H` is the second calendar half of the current year,
|
|
1485
|
-
# * `YYYY-3Q` or `YYYY-Q3` is the third calendar quarter of year YYYY,
|
|
1486
|
-
# * `Q3` or `3Q` is the third calendar quarter in the current year,
|
|
1487
|
-
# * `YYYY-04` or `YYYY-4` is April, the fourth month of year `YYYY`,
|
|
1488
|
-
# * `4-12` or `04-12` is the 12th of April in the current year,
|
|
1489
|
-
# * `4` or `04` is April in the current year,
|
|
1490
|
-
# * `YYYY-W32` or `YYYY-32W` is the 32nd week in year YYYY,
|
|
1491
|
-
# * `W32` or `32W` is the 32nd week in the current year,
|
|
1492
|
-
# * `W32-4` or `32W-4` is the 4th day of the 32nd week in the current year,
|
|
1493
|
-
# * `YYYY-MM-I` or `YYYY-MM-II` is the first or second half of the given month,
|
|
1494
|
-
# * `YYYY-MM-i` or `YYYY-MM-v` is the first or fifth week of the given month,
|
|
1495
|
-
# * `MM-i` or `MM-v` is the first or fifth week of the current month,
|
|
1496
|
-
# * `YYYY-MM-3Tu` or `YYYY-MM-4Mo` is the third Tuesdsay and fourth Monday of the given month,
|
|
1497
|
-
# * `MM-3Tu` or `MM-4Mo` is the third Tuesdsay and fourth Monday of the given month in the current year,
|
|
1498
|
-
# * `3Tu` or `4Mo` is the third Tuesdsay and fourth Monday of the current month,
|
|
1499
|
-
# * `YYYY-E` is Easter of the given year YYYY,
|
|
1500
|
-
# * `E` is Easter of the current year YYYY,
|
|
1501
|
-
# * `YYYY-E+50` and `YYYY-E-40` is 50 days after and 40 days before Easter of the given year,
|
|
1502
|
-
# * `E+50` and `E-40` is 50 days after and 40 days before Easter of the current year,
|
|
1503
|
-
# * `YYYY-001` and `YYYY-182` is first and 182nd day of the given year,
|
|
1504
|
-
# * `001` and `182` is first and 182nd day of the current year,
|
|
1505
|
-
# * `this_<chunk>` where `<chunk>` is one of `year`, `half`, `quarter`,
|
|
1506
|
-
# `bimonth`, `month`, `semimonth`, `biweek`, `week`, or `day`, the
|
|
1507
|
-
# corresponding calendar period in which the current date falls,
|
|
1508
|
-
# * `last_<chunk>` where `<chunk>` is one of `year`, `half`, `quarter`,
|
|
1509
|
-
# `bimonth`, `month`, `semimonth`, `biweek`, `week`, or `day`, the
|
|
1510
|
-
# corresponding calendar period immediately before the one in which the
|
|
1511
|
-
# current date falls,
|
|
1512
|
-
# * `today` is the same as `this_day`,
|
|
1513
|
-
# * `yesterday` is the same as `last_day`,
|
|
1514
|
-
# * `forever` is the period from ::Date::BOT to ::Date::EOT, essentially all
|
|
1515
|
-
# dates of commercial interest, and
|
|
1516
|
-
# * `never` causes the method to return nil.
|
|
1517
|
-
#
|
|
1518
|
-
# In all of the above example specs, letter used for calendar chunks, `W`,
|
|
1519
|
-
# `Q`, and `H` can be written in lower case as well. Also, you can use `/`
|
|
1520
|
-
# to separate date components instead of `-`.
|
|
1521
|
-
#
|
|
1522
|
-
# @example
|
|
1523
|
-
# ::Date.parse_spec('2012-W32').iso # => "2012-08-06"
|
|
1524
|
-
# ::Date.parse_spec('2012-W32', :to).iso # => "2012-08-12"
|
|
1525
|
-
# ::Date.parse_spec('W32').iso # => "2012-08-06" if executed in 2012
|
|
1526
|
-
# ::Date.parse_spec('W32').iso # => "2012-08-04" if executed in 2014
|
|
1527
|
-
#
|
|
1528
|
-
# @param spec [String, #to_s] the spec to be interpreted as a calendar period
|
|
1529
|
-
#
|
|
1530
|
-
# @param spec_type [Symbol, :from, :to] return the first (:from) or last (:to)
|
|
1531
|
-
# date in the spec's period respectively
|
|
1532
|
-
#
|
|
1533
|
-
# @return [::Date] date that is the first (:from) or last (:to) in the period
|
|
1534
|
-
# designated by spec
|
|
1535
|
-
def parse_spec(spec, spec_type = :from)
|
|
1536
|
-
spec = spec.to_s.strip.clean
|
|
1537
|
-
unless %i[from to].include?(spec_type)
|
|
1538
|
-
raise ArgumentError, "invalid date spec type: '#{spec_type}'"
|
|
1539
|
-
end
|
|
1540
|
-
|
|
1541
|
-
today = ::Date.current
|
|
1542
|
-
case spec
|
|
1543
|
-
when %r{\A((?<yr>\d\d\d\d)[-/])?(?<doy>\d\d\d)\z}
|
|
1544
|
-
# With 3-digit YYYY-ddd, return the day-of-year
|
|
1545
|
-
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
|
1546
|
-
doy = Regexp.last_match[:doy].to_i
|
|
1547
|
-
max_doy = ::Date.gregorian_leap?(year) ? 366 : 365
|
|
1548
|
-
if doy > max_doy
|
|
1549
|
-
raise ArgumentError, "invalid day-of-year '#{doy}' (1..#{max_doy}) in '#{spec}'"
|
|
1550
|
-
end
|
|
1551
|
-
|
|
1552
|
-
::Date.new(year, 1, 1) + doy - 1
|
|
1553
|
-
when %r{\A((?<yr>\d\d\d\d)[-/])?(?<mo>\d\d?)([-/](?<dy>\d\d?))?\z}
|
|
1554
|
-
# MM, YYYY-MM, MM-DD
|
|
1555
|
-
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
|
1556
|
-
month = Regexp.last_match[:mo].to_i
|
|
1557
|
-
day = Regexp.last_match[:dy]&.to_i
|
|
1558
|
-
unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
|
|
1559
|
-
raise ArgumentError, "invalid month number (1-12): '#{spec}'"
|
|
1560
|
-
end
|
|
1561
|
-
|
|
1562
|
-
if day
|
|
1563
|
-
::Date.new(year, month, day)
|
|
1564
|
-
elsif spec_type == :from
|
|
1565
|
-
::Date.new(year, month, 1)
|
|
1566
|
-
else
|
|
1567
|
-
::Date.new(year, month, 1).end_of_month
|
|
1568
|
-
end
|
|
1569
|
-
when %r{\A((?<yr>\d\d\d\d)[-/])?(?<wk>\d\d?)W(-(?<dy>\d))?\z}xi,
|
|
1570
|
-
%r{\A((?<yr>\d\d\d\d)[-/])?W(?<wk>\d\d?)(-(?<dy>\d))?\z}xi
|
|
1571
|
-
# Commercial week numbers. The first commercial week of the year is
|
|
1572
|
-
# the one that includes the first Thursday of that year. In the
|
|
1573
|
-
# Gregorian calendar, this is equivalent to the week which includes
|
|
1574
|
-
# January 4. This appears to be the equivalent of ISO 8601 week
|
|
1575
|
-
# number as described at https://en.wikipedia.org/wiki/ISO_week_date
|
|
1576
|
-
year = Regexp.last_match[:yr]&.to_i
|
|
1577
|
-
week_num = Regexp.last_match[:wk].to_i
|
|
1578
|
-
day = Regexp.last_match[:dy]&.to_i
|
|
1579
|
-
unless (1..53).cover?(week_num)
|
|
1580
|
-
raise ArgumentError, "invalid week number (1-53): '#{spec}'"
|
|
1581
|
-
end
|
|
1582
|
-
if day && !(1..7).cover?(day)
|
|
1583
|
-
raise ArgumentError, "invalid ISO day number (1-7): '#{spec}'"
|
|
1584
|
-
end
|
|
1585
|
-
|
|
1586
|
-
if spec_type == :from
|
|
1587
|
-
::Date.commercial(year ? year : today.year, week_num, day ? day : 1)
|
|
1588
|
-
else
|
|
1589
|
-
::Date.commercial(year ? year : today.year, week_num, day ? day : 7)
|
|
1590
|
-
end
|
|
1591
|
-
when %r{^((?<yr>\d\d\d\d)[-/])?(?<qt>\d)[Qq]$}, %r{^((?<yr>\d\d\d\d)[-/])?[Qq](?<qt>\d)$}
|
|
1592
|
-
# Year-Quarter
|
|
1593
|
-
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
|
1594
|
-
quarter = Regexp.last_match[:qt].to_i
|
|
1595
|
-
unless [1, 2, 3, 4].include?(quarter)
|
|
1596
|
-
raise ArgumentError, "invalid quarter number (1-4): '#{spec}'"
|
|
1597
|
-
end
|
|
1598
|
-
|
|
1599
|
-
month = quarter * 3
|
|
1600
|
-
if spec_type == :from
|
|
1601
|
-
::Date.new(year, month, 1).beginning_of_quarter
|
|
1602
|
-
else
|
|
1603
|
-
::Date.new(year, month, 1).end_of_quarter
|
|
1604
|
-
end
|
|
1605
|
-
when %r{^((?<yr>\d\d\d\d)[-/])?(?<hf>\d)[Hh]$}, %r{^((?<yr>\d\d\d\d)[-/])?[Hh](?<hf>\d)$}
|
|
1606
|
-
# Year-Half
|
|
1607
|
-
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
|
1608
|
-
half = Regexp.last_match[:hf].to_i
|
|
1609
|
-
msg = "invalid half number: '#{spec}'"
|
|
1610
|
-
raise ArgumentError, msg unless [1, 2].include?(half)
|
|
1611
|
-
|
|
1612
|
-
month = half * 6
|
|
1613
|
-
if spec_type == :from
|
|
1614
|
-
::Date.new(year, month, 15).beginning_of_half
|
|
1615
|
-
else
|
|
1616
|
-
::Date.new(year, month, 1).end_of_half
|
|
1617
|
-
end
|
|
1618
|
-
when /\A(?<yr>\d\d\d\d)\z/
|
|
1619
|
-
# Year only
|
|
1620
|
-
year = Regexp.last_match[:yr].to_i
|
|
1621
|
-
if spec_type == :from
|
|
1622
|
-
::Date.new(year, 1, 1)
|
|
1623
|
-
else
|
|
1624
|
-
::Date.new(year, 12, 31)
|
|
1625
|
-
end
|
|
1626
|
-
when %r{\A(?<yr>\d\d\d\d)[-/](?<mo>\d\d?)[-/](?<hf_mo>(I|II))\z}
|
|
1627
|
-
# Year, month, half-month, designated with uppercase Roman
|
|
1628
|
-
year = Regexp.last_match[:yr].to_i
|
|
1629
|
-
month = Regexp.last_match[:mo].to_i
|
|
1630
|
-
hf_mo = Regexp.last_match[:hf_mo]
|
|
1631
|
-
if hf_mo == "I"
|
|
1632
|
-
spec_type == :from ? ::Date.new(year, month, 1) : ::Date.new(year, month, 15)
|
|
1633
|
-
else
|
|
1634
|
-
# hf_mo == "II"
|
|
1635
|
-
spec_type == :from ? ::Date.new(year, month, 16) : ::Date.new(year, month, 16).end_of_month
|
|
1636
|
-
end
|
|
1637
|
-
when %r{\A((?<yr>\d\d\d\d)[-/])?((?<mo>\d\d?)[-/])?(?<wk>(i|ii|iii|iv|v|vi))\z}
|
|
1638
|
-
# Year, month, week-of-month, partial-or-whole, designated with lowercase Roman
|
|
1639
|
-
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
|
1640
|
-
month = Regexp.last_match[:mo]&.to_i || ::Date.today.month
|
|
1641
|
-
wk = ['i', 'ii', 'iii', 'iv', 'v', 'vi'].index(Regexp.last_match[:wk]) + 1
|
|
1642
|
-
result =
|
|
1643
|
-
if spec_type == :from
|
|
1644
|
-
::Date.new(year, month, 1).beginning_of_week + (wk - 1).weeks
|
|
1645
|
-
else
|
|
1646
|
-
::Date.new(year, month, 1).end_of_week + (wk - 1).weeks
|
|
1647
|
-
end
|
|
1648
|
-
# If beginning of week of the 1st is in prior month, return the 1st
|
|
1649
|
-
result = [result, ::Date.new(year, month, 1)].max
|
|
1650
|
-
# If the whole week of the result is in the next month, there was no such week
|
|
1651
|
-
if result.beginning_of_week.month > month
|
|
1652
|
-
msg = sprintf("no week number #{wk} in %04d-%02d", year, month)
|
|
1653
|
-
raise ArgumentError, msg
|
|
1654
|
-
else
|
|
1655
|
-
# But if part of the result week is in this month, return end of month
|
|
1656
|
-
[result, ::Date.new(year, month, 1).end_of_month].min
|
|
1657
|
-
end
|
|
1658
|
-
when %r{\A((?<yr>\d\d\d\d)[-/])?((?<mo>\d\d?)[-/])?((?<ndow>\d+)(?<dow>Su|Mo|Tu|We|Th|Fr|Sa))\z}
|
|
1659
|
-
# Year, month, week-of-month, partial-or-whole, designated with lowercase Roman
|
|
1660
|
-
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
|
1661
|
-
month = Regexp.last_match[:mo]&.to_i || ::Date.today.month
|
|
1662
|
-
ndow = Regexp.last_match[:ndow].to_i
|
|
1663
|
-
dow = ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'].index(Regexp.last_match[:dow]) ||
|
|
1664
|
-
['su', 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su'].index(Regexp.last_match[:dow])
|
|
1665
|
-
unless (1..12).cover?(month)
|
|
1666
|
-
raise ArgumentError, "invalid month number (1-12): '#{month}' in '#{spec}'"
|
|
1667
|
-
end
|
|
1668
|
-
unless (1..5).cover?(ndow)
|
|
1669
|
-
raise ArgumentError, "invalid ordinal day number (1-5): '#{ndow}' in '#{spec}'"
|
|
1670
|
-
end
|
|
1671
|
-
|
|
1672
|
-
::Date.nth_wday_in_year_month(ndow, dow, year, month)
|
|
1673
|
-
when %r{\A(?<yr>\d\d\d\d[-/])?E(?<off>[+-]\d+)?\z}i
|
|
1674
|
-
# Easter for the given year, current year (if no year component),
|
|
1675
|
-
# optionally plus or minus a day offset
|
|
1676
|
-
year = Regexp.last_match[:yr]&.to_i || ::Date.today.year
|
|
1677
|
-
offset = Regexp.last_match[:off]&.to_i || 0
|
|
1678
|
-
::Date.easter(year) + offset
|
|
1679
|
-
when %r{\A(?<rel>(to[_-]?|this[_-]?)|(last[_-]?|yester[_-]?|next[_-]?))
|
|
1680
|
-
(?<chunk>morrow|day|week|biweek|semimonth|bimonth|month|quarter|half|year)\z}xi
|
|
1681
|
-
rel = Regexp.last_match[:rel]
|
|
1682
|
-
chunk = Regexp.last_match[:chunk].to_sym
|
|
1683
|
-
if chunk.match?(/morrow/i)
|
|
1684
|
-
chunk = :day
|
|
1685
|
-
rel = 'next'
|
|
1686
|
-
end
|
|
1687
|
-
start =
|
|
1688
|
-
if rel.match?(/this|to/i)
|
|
1689
|
-
::Date.today
|
|
1690
|
-
elsif rel.match?(/next/i)
|
|
1691
|
-
::Date.today.add_chunk(chunk, 1)
|
|
1692
|
-
else
|
|
1693
|
-
# rel.match?(/last|yester/i)
|
|
1694
|
-
::Date.today.add_chunk(chunk, -1)
|
|
1695
|
-
end
|
|
1696
|
-
if spec_type == :from
|
|
1697
|
-
start.beginning_of_chunk(chunk)
|
|
1698
|
-
else
|
|
1699
|
-
start.end_of_chunk(chunk)
|
|
1700
|
-
end
|
|
1701
|
-
when /^forever/i
|
|
1702
|
-
if spec_type == :from
|
|
1703
|
-
::Date::BOT
|
|
1704
|
-
else
|
|
1705
|
-
::Date::EOT
|
|
1706
|
-
end
|
|
1707
|
-
when /^never/i
|
|
1708
|
-
nil
|
|
1709
|
-
else
|
|
1710
|
-
raise ArgumentError, "bad date spec: '#{spec}''"
|
|
1711
|
-
end
|
|
1712
|
-
end
|
|
1713
|
-
|
|
1714
|
-
# @group Utilities
|
|
1715
|
-
|
|
1716
|
-
# An Array of the number of days in each month indexed by month number,
|
|
1717
|
-
# starting with January = 1, etc.
|
|
1718
|
-
COMMON_YEAR_DAYS_IN_MONTH = [
|
|
1719
|
-
31,
|
|
1720
|
-
31,
|
|
1721
|
-
28,
|
|
1722
|
-
31,
|
|
1723
|
-
30,
|
|
1724
|
-
31,
|
|
1725
|
-
30,
|
|
1726
|
-
31,
|
|
1727
|
-
31,
|
|
1728
|
-
30,
|
|
1729
|
-
31,
|
|
1730
|
-
30,
|
|
1731
|
-
31
|
|
1732
|
-
].freeze
|
|
1733
|
-
def days_in_month(year, month)
|
|
1734
|
-
raise ArgumentError, 'illegal month number' if month < 1 || month > 12
|
|
1735
|
-
|
|
1736
|
-
days = COMMON_YEAR_DAYS_IN_MONTH[month]
|
|
1737
|
-
if month == 2
|
|
1738
|
-
::Date.new(year, month, 1).leap? ? 29 : 28
|
|
1739
|
-
else
|
|
1740
|
-
days
|
|
1741
|
-
end
|
|
1742
|
-
end
|
|
1743
|
-
|
|
1744
|
-
# Return the 1-indexed integer that corresponds to a month name.
|
|
1745
|
-
#
|
|
1746
|
-
# @param name [String] a name of a month
|
|
1747
|
-
#
|
|
1748
|
-
# @return [Integer] the integer integer that corresponds to a month
|
|
1749
|
-
# name, or nil of no month recognized.
|
|
1750
|
-
def mo_name_to_num(name)
|
|
1751
|
-
case name.clean
|
|
1752
|
-
when /\Ajan/i
|
|
1753
|
-
1
|
|
1754
|
-
when /\Afeb/i
|
|
1755
|
-
2
|
|
1756
|
-
when /\Amar/i
|
|
1757
|
-
3
|
|
1758
|
-
when /\Aapr/i
|
|
1759
|
-
4
|
|
1760
|
-
when /\Amay/i
|
|
1761
|
-
5
|
|
1762
|
-
when /\Ajun/i
|
|
1763
|
-
6
|
|
1764
|
-
when /\Ajul/i
|
|
1765
|
-
7
|
|
1766
|
-
when /\Aaug/i
|
|
1767
|
-
8
|
|
1768
|
-
when /\Asep/i
|
|
1769
|
-
9
|
|
1770
|
-
when /\Aoct/i
|
|
1771
|
-
10
|
|
1772
|
-
when /\Anov/i
|
|
1773
|
-
11
|
|
1774
|
-
when /\Adec/i
|
|
1775
|
-
12
|
|
1776
|
-
end
|
|
1777
|
-
end
|
|
1778
|
-
|
|
1779
|
-
# Return the nth weekday in the given month. If n is negative, count from
|
|
1780
|
-
# last day of month.
|
|
1781
|
-
#
|
|
1782
|
-
# @param nth [Integer] the ordinal number for the weekday
|
|
1783
|
-
# @param wday [Integer] the weekday of interest with Sunday 0 to Saturday 6
|
|
1784
|
-
# @param year [Integer] the year of interest
|
|
1785
|
-
# @param month [Integer] the month of interest with January 1 to December 12
|
|
1786
|
-
def nth_wday_in_year_month(nth, wday, year, month)
|
|
1787
|
-
wday = wday.to_i
|
|
1788
|
-
raise ArgumentError, 'illegal weekday number' if wday.negative? || wday > 6
|
|
1789
|
-
|
|
1790
|
-
month = month.to_i
|
|
1791
|
-
raise ArgumentError, 'illegal month number' if month < 1 || month > 12
|
|
1792
|
-
|
|
1793
|
-
nth = nth.to_i
|
|
1794
|
-
if nth.abs > 5
|
|
1795
|
-
raise ArgumentError, "#{nth.abs} out of range: can be no more than 5 of any day of the week in any month"
|
|
1796
|
-
end
|
|
1797
|
-
|
|
1798
|
-
result =
|
|
1799
|
-
if nth.positive?
|
|
1800
|
-
# Set d to the 1st wday in month
|
|
1801
|
-
d = ::Date.new(year, month, 1)
|
|
1802
|
-
d += 1 while d.wday != wday
|
|
1803
|
-
# Set d to the nth wday in month
|
|
1804
|
-
nd = 1
|
|
1805
|
-
while nd != nth
|
|
1806
|
-
d += 7
|
|
1807
|
-
nd += 1
|
|
1808
|
-
end
|
|
1809
|
-
d
|
|
1810
|
-
elsif nth.negative?
|
|
1811
|
-
nth = -nth
|
|
1812
|
-
# Set d to the last wday in month
|
|
1813
|
-
d = ::Date.new(year, month, 1).end_of_month
|
|
1814
|
-
d -= 1 while d.wday != wday
|
|
1815
|
-
# Set d to the nth wday in month
|
|
1816
|
-
nd = 1
|
|
1817
|
-
while nd != nth
|
|
1818
|
-
d -= 7
|
|
1819
|
-
nd += 1
|
|
1820
|
-
end
|
|
1821
|
-
d
|
|
1822
|
-
else
|
|
1823
|
-
raise ArgumentError, 'Argument nth cannot be zero'
|
|
1824
|
-
end
|
|
1825
|
-
|
|
1826
|
-
dow = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][wday]
|
|
1827
|
-
if result.month != month
|
|
1828
|
-
raise ArgumentError, "There is no #{nth}th #{dow} in #{year}-#{month}"
|
|
1829
|
-
end
|
|
1830
|
-
|
|
1831
|
-
result
|
|
1832
|
-
end
|
|
1833
|
-
|
|
1834
|
-
# Return the date of Easter for the Western Church in the given year.
|
|
1835
|
-
#
|
|
1836
|
-
# @param year [Integer] the year of interest
|
|
1837
|
-
# @return [::Date] the date of Easter for year
|
|
1838
|
-
def easter(year)
|
|
1839
|
-
year = year.to_i
|
|
1840
|
-
y = year
|
|
1841
|
-
a = y % 19
|
|
1842
|
-
b, c = y.divmod(100)
|
|
1843
|
-
d, e = b.divmod(4)
|
|
1844
|
-
f = (b + 8) / 25
|
|
1845
|
-
g = (b - f + 1) / 3
|
|
1846
|
-
h = (19 * a + b - d - g + 15) % 30
|
|
1847
|
-
i, k = c.divmod(4)
|
|
1848
|
-
l = (32 + 2 * e + 2 * i - h - k) % 7
|
|
1849
|
-
m = (a + 11 * h + 22 * l) / 451
|
|
1850
|
-
n, p = (h + l - 7 * m + 114).divmod(31)
|
|
1851
|
-
::Date.new(y, n, p + 1)
|
|
1852
|
-
end
|
|
1853
|
-
|
|
1854
|
-
# Ensure that date is of class Date based either on a string or Date
|
|
1855
|
-
# object or an object that responds to #to_date or #to_datetime. If the
|
|
1856
|
-
# given object is a String, use Date.parse to try to convert it.
|
|
1857
|
-
#
|
|
1858
|
-
# If given a DateTime, it returns the unrounded DateTime; if given a
|
|
1859
|
-
# Time, it converts it to a DateTime.
|
|
1860
|
-
#
|
|
1861
|
-
# @param dat [String, Date, Time] the object to be converted to Date
|
|
1862
|
-
# @return [Date, DateTime]
|
|
1863
|
-
def ensure_date(dat)
|
|
1864
|
-
if dat.is_a?(Date) || dat.is_a?(DateTime)
|
|
1865
|
-
dat
|
|
1866
|
-
elsif dat.is_a?(Time)
|
|
1867
|
-
dat.to_datetime
|
|
1868
|
-
elsif dat.respond_to?(:to_datetime)
|
|
1869
|
-
dat.to_datetime
|
|
1870
|
-
elsif dat.respond_to?(:to_date)
|
|
1871
|
-
dat.to_date
|
|
1872
|
-
elsif dat.is_a?(String)
|
|
1873
|
-
begin
|
|
1874
|
-
::Date.parse(dat)
|
|
1875
|
-
rescue ::Date::Error
|
|
1876
|
-
raise ArgumentError, "cannot convert string '#{dat}' to a Date or DateTime"
|
|
1877
|
-
end
|
|
1878
|
-
else
|
|
1879
|
-
raise ArgumentError, "cannot convert class '#{dat.class}' to a Date or DateTime"
|
|
1880
|
-
end
|
|
1881
|
-
end
|
|
1882
|
-
end
|
|
1883
|
-
|
|
1884
|
-
def self.included(base)
|
|
1885
|
-
base.extend(ClassMethods)
|
|
1886
|
-
end
|
|
1887
|
-
end
|
|
1888
|
-
end
|
|
1889
|
-
|
|
1890
|
-
class Date
|
|
1891
|
-
include FatCore::Date
|
|
1892
|
-
def self.ensure(dat)
|
|
1893
|
-
ensure_date(dat)
|
|
1894
|
-
end
|
|
1895
|
-
# @!parse include FatCore::Date
|
|
1896
|
-
# @!parse extend FatCore::Date::ClassMethods
|
|
1897
|
-
end
|