fat_core 2.0.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/fat_core/date.rb CHANGED
@@ -1,1033 +1,1046 @@
1
- class Date
2
- # Constants for Begining of Time (BOT) and End of Time (EOT)
3
- # Both outside the range of what we would find in an accounting app.
4
- BOT = Date.parse('1900-01-01')
5
- EOT = Date.parse('3000-12-31')
6
-
7
- # Convert a string with an American style date into a Date object
8
- #
9
- # An American style date is of the form MM/DD/YYYY, that is it places the
10
- # month first, then the day of the month, and finally the year. The
11
- # European convention is to place the day of the month first, DD/MM/YYYY.
12
- # Because a date found in the wild can be ambiguous, e.g. 3/5/2014, a date
13
- # string known to be using the American convention can be parsed using this
14
- # method. Both the month and the day can be a single digit. The year can
15
- # be either 2 or 4 digits, and if given as 2 digits, it adds 2000 to it to
16
- # give the year.
17
- #
18
- # @example
19
- # Date.parse_american('9/11/2001') #=> Date(2011, 9, 11)
20
- # Date.parse_american('9/11/01') #=> Date(2011, 9, 11)
21
- # Date.parse_american('9/11/1') #=> ArgumentError
22
- #
23
- # @param str [#to_s] a stringling of the form MM/DD/YYYY
24
- # @return [Date] the date represented by the string paramenter.
25
- def self.parse_american(str)
26
- unless str.to_s =~ %r{\A\s*(\d\d?)\s*/\s*(\d\d?)\s*/\s*(\d?\d?\d\d)\s*\z}
27
- raise ArgumentError, "date string must be of form 'MM?/DD?/YY(YY)?'"
1
+ require 'date'
2
+ require 'active_support/core_ext/date'
3
+ require 'active_support/core_ext/time'
4
+ require 'active_support/core_ext/numeric/time'
5
+ require 'active_support/core_ext/integer/time'
6
+ require 'fat_core/string'
7
+
8
+ module FatCore
9
+ module Date
10
+ # Constants for Begining of Time (BOT) and End of Time (EOT)
11
+ # Both outside the range of what we would find in an accounting app.
12
+ ::Date::BOT = ::Date.parse('1900-01-01')
13
+ ::Date::EOT = ::Date.parse('3000-12-31')
14
+
15
+ # Predecessor of self. Allows Date to work as a countable element
16
+ # of a Range.
17
+ def pred
18
+ self - 1.day
28
19
  end
29
- year = $3.to_i
30
- month = $1.to_i
31
- day = $2.to_i
32
- year += 2000 if year < 100
33
- Date.new(year, month, day)
34
- end
35
20
 
36
- # Convert a 'date spec' to a Date. A date spec is a short-hand way of
37
- # specifying a date, relative to the computer clock. A date spec can
38
- # interpreted as either a 'from spec' or a 'to spec'.
39
- # @example
40
- #
41
- # Assuming that Date.current at the time of execution is 2014-07-26 and
42
- # using the default spec_type of :from. The return values are actually Date
43
- # objects, but are shown below as textual dates.
44
- #
45
- # A fully specified date returns that date:
46
- # Date.parse_spec('2001-09-11') # =>
47
- # Commercial weeks can be specified using, for example W32 or 32W, with the
48
- # week beginning on Monday, ending on Sunday.
49
- # Date.parse_spec('2012-W32') # =>
50
- # Date.parse_spec('2012-W32', :to) # =>
51
- # Date.parse_spec('W32') # =>
52
- #
53
- # A spec of the form Q3 or 3Q returns the beginning or end of calendar
54
- # quarters.
55
- # Date.parse_spec('Q3') # =>
56
- #
57
- # @param spec [#to_s] a stringling containing the spec to be interpreted
58
- # @param spec_type [:from, :to] interpret the spec as a from- or to-spec
59
- # respectively, defaulting to interpretation as a to-spec.
60
- # @return [Date] a date object equivalent to the date spec
61
- def self.parse_spec(spec, spec_type = :from)
62
- spec = spec.to_s.strip
63
- unless [:from, :to].include?(spec_type)
64
- raise ArgumentError, "invalid date spec type: '#{spec_type}'"
21
+ # Successor of self. Allows Date to work as a countable element
22
+ # of a Range.
23
+ def succ
24
+ self + 1.day
65
25
  end
66
26
 
67
- today = Date.current
68
- case spec.clean
69
- when /\A(\d\d\d\d)[-\/](\d\d?)[-\/](\d\d?)\z/
70
- # A specified date
71
- Date.new($1.to_i, $2.to_i, $3.to_i)
72
- when /\AW(\d\d?)\z/, /\A(\d\d?)W\z/
73
- week_num = $1.to_i
74
- if week_num < 1 || week_num > 53
75
- raise ArgumentError, "invalid week number (1-53): 'W#{week_num}'"
76
- end
77
- if spec_type == :from
78
- Date.commercial(today.year, week_num).beginning_of_week
79
- else
80
- Date.commercial(today.year, week_num).end_of_week
81
- end
82
- when /\A(\d\d\d\d)[-\/]W(\d\d?)\z/, /\A(\d\d\d\d)[-\/](\d\d?)W\z/
83
- year = $1.to_i
84
- week_num = $2.to_i
85
- if week_num < 1 || week_num > 53
86
- raise ArgumentError, "invalid week number (1-53): 'W#{week_num}'"
87
- end
88
- if spec_type == :from
89
- Date.commercial(year, week_num).beginning_of_week
90
- else
91
- Date.commercial(year, week_num).end_of_week
92
- end
93
- when /^(\d\d\d\d)[-\/](\d)[Qq]$/, /^(\d\d\d\d)[-\/][Qq](\d)$/
94
- # Year-Quarter
95
- year = $1.to_i
96
- quarter = $2.to_i
97
- unless [1, 2, 3, 4].include?(quarter)
98
- raise ArgumentError, "bad date format: #{spec}"
99
- end
100
- month = quarter * 3
101
- if spec_type == :from
102
- Date.new(year, month, 1).beginning_of_quarter
103
- else
104
- Date.new(year, month, 1).end_of_quarter
105
- end
106
- when /^([1234])[qQ]$/, /^[qQ]([1234])$/
107
- # Quarter only
108
- this_year = today.year
109
- quarter = $1.to_i
110
- date = Date.new(this_year, quarter * 3, 15)
111
- if spec_type == :from
112
- date.beginning_of_quarter
113
- else
114
- date.end_of_quarter
115
- end
116
- when /^(\d\d\d\d)[-\/](\d)[Hh]$/, /^(\d\d\d\d)[-\/][Hh](\d)$/
117
- # Year-Half
118
- year = $1.to_i
119
- half = $2.to_i
120
- unless [1, 2].include?(half)
121
- raise ArgumentError, "bad date format: #{spec}"
122
- end
123
- month = half * 6
124
- if spec_type == :from
125
- Date.new(year, month, 15).beginning_of_half
126
- else
127
- Date.new(year, month, 1).end_of_half
128
- end
129
- when /^([12])[hH]$/, /^[hH]([12])$/
130
- # Half only
131
- this_year = today.year
132
- half = $1.to_i
133
- date = Date.new(this_year, half * 6, 15)
134
- if spec_type == :from
135
- date.beginning_of_half
136
- else
137
- date.end_of_half
138
- end
139
- when /^(\d\d\d\d)[-\/](\d\d?)*$/
140
- # Year-Month only
141
- if spec_type == :from
142
- Date.new($1.to_i, $2.to_i, 1)
143
- else
144
- Date.new($1.to_i, $2.to_i, 1).end_of_month
145
- end
146
- when /^(\d\d?)[-\/](\d\d?)*$/
147
- # Month-Day only
148
- if spec_type == :from
149
- Date.new(today.year, $1.to_i, $2.to_i)
150
- else
151
- Date.new(today.year, $1.to_i, $2.to_i).end_of_month
152
- end
153
- when /\A(\d\d?)\z/
154
- # Month only
155
- if spec_type == :from
156
- Date.new(today.year, $1.to_i, 1)
157
- else
158
- Date.new(today.year, $1.to_i, 1).end_of_month
159
- end
160
- when /^(\d\d\d\d)$/
161
- # Year only
162
- if spec_type == :from
163
- Date.new($1.to_i, 1, 1)
164
- else
165
- Date.new($1.to_i, 12, 31)
166
- end
167
- when /^(to|this_?)?day/
168
- today
169
- when /^(yester|last_?)?day/
170
- today - 1.day
171
- when /^(this_?)?week/
172
- spec_type == :from ? today.beginning_of_week : today.end_of_week
173
- when /last_?week/
174
- if spec_type == :from
175
- (today - 1.week).beginning_of_week
176
- else
177
- (today - 1.week).end_of_week
178
- end
179
- when /^(this_?)?biweek/
180
- if spec_type == :from
181
- today.beginning_of_biweek
182
- else
183
- today.end_of_biweek
184
- end
185
- when /last_?biweek/
186
- if spec_type == :from
187
- (today - 2.week).beginning_of_biweek
188
- else
189
- (today - 2.week).end_of_biweek
190
- end
191
- when /^(this_?)?semimonth/
192
- spec_type == :from ? today.beginning_of_semimonth : today.end_of_semimonth
193
- when /^last_?semimonth/
194
- if spec_type == :from
195
- (today - 15.days).beginning_of_semimonth
196
- else
197
- (today - 15.days).end_of_semimonth
198
- end
199
- when /^(this_?)?month/
200
- if spec_type == :from
201
- today.beginning_of_month
202
- else
203
- today.end_of_month
204
- end
205
- when /^last_?month/
206
- if spec_type == :from
207
- (today - 1.month).beginning_of_month
208
- else
209
- (today - 1.month).end_of_month
27
+ # Format as an ISO string.
28
+ def iso
29
+ strftime('%Y-%m-%d')
30
+ end
31
+
32
+ # Format date to TeX documents as ISO strings
33
+ def tex_quote
34
+ iso
35
+ end
36
+
37
+ # Format as an all-numeric string, i.e. 'YYYYMMDD'
38
+ def num
39
+ strftime('%Y%m%d')
40
+ end
41
+
42
+ # Format as an inactive Org date (see emacs org-mode)
43
+ def org
44
+ strftime('[%Y-%m-%d %a]')
45
+ end
46
+
47
+ # Format as an English string
48
+ def eng
49
+ strftime('%B %e, %Y')
50
+ end
51
+
52
+ # Format date in MM/DD/YYYY form, as typical for the short American
53
+ # form.
54
+ def american
55
+ strftime '%-m/%-d/%Y'
56
+ end
57
+
58
+ # Does self fall on a weekend?
59
+ def weekend?
60
+ saturday? || sunday?
61
+ end
62
+
63
+ # Does self fall on a weekday?
64
+ def weekday?
65
+ !weekend?
66
+ end
67
+
68
+ # Self's calendar half: 1 or 2
69
+ def half
70
+ case month
71
+ when (1..6)
72
+ 1
73
+ when (7..12)
74
+ 2
210
75
  end
211
- when /^(this_?)?bimonth/
212
- if spec_type == :from
213
- today.beginning_of_bimonth
214
- else
215
- today.end_of_bimonth
76
+ end
77
+
78
+ # Self's calendar quarter: 1, 2, 3, or 4
79
+ def quarter
80
+ case month
81
+ when (1..3)
82
+ 1
83
+ when (4..6)
84
+ 2
85
+ when (7..9)
86
+ 3
87
+ when (10..12)
88
+ 4
216
89
  end
217
- when /^last_?bimonth/
218
- if spec_type == :from
219
- (today - 2.month).beginning_of_bimonth
90
+ end
91
+
92
+ # The date that is the first day of the half-year in which self falls.
93
+ def beginning_of_half
94
+ if month > 9
95
+ (beginning_of_quarter - 15).beginning_of_quarter
96
+ elsif month > 6
97
+ beginning_of_quarter
220
98
  else
221
- (today - 2.month).end_of_bimonth
99
+ beginning_of_year
222
100
  end
223
- when /^(this_?)?quarter/
224
- if spec_type == :from
225
- today.beginning_of_quarter
101
+ end
102
+
103
+ # The date that is the last day of the half-year in which self falls.
104
+ def end_of_half
105
+ if month < 4
106
+ (end_of_quarter + 15).end_of_quarter
107
+ elsif month < 7
108
+ end_of_quarter
226
109
  else
227
- today.end_of_quarter
110
+ end_of_year
228
111
  end
229
- when /^last_?quarter/
230
- if spec_type == :from
231
- (today - 3.months).beginning_of_quarter
112
+ end
113
+
114
+ # The date that is the first day of the bimonth in which self
115
+ # falls. A 'bimonth' is a two-month calendar period beginning on the
116
+ # first day of the odd-numbered months. E.g., 2014-01-01 to
117
+ # 2014-02-28 is the first bimonth of 2014.
118
+ def beginning_of_bimonth
119
+ if month.odd?
120
+ beginning_of_month
232
121
  else
233
- (today - 3.months).end_of_quarter
122
+ (self - 1.month).beginning_of_month
234
123
  end
235
- when /^(this_?)?half/
236
- if spec_type == :from
237
- today.beginning_of_half
124
+ end
125
+
126
+ # The date that is the last day of the bimonth in which self falls.
127
+ # A 'bimonth' is a two-month calendar period beginning on the first
128
+ # day of the odd-numbered months. E.g., 2014-01-01 to 2014-02-28 is
129
+ # the first bimonth of 2014.
130
+ def end_of_bimonth
131
+ if month.odd?
132
+ (self + 1.month).end_of_month
238
133
  else
239
- today.end_of_half
134
+ end_of_month
240
135
  end
241
- when /^last_?half/
242
- if spec_type == :from
243
- (today - 6.months).beginning_of_half
136
+ end
137
+
138
+ # The date that is the first day of the semimonth in which self
139
+ # falls. A semimonth is a calendar period beginning on the 1st or
140
+ # 16th of each month and ending on the 15th or last day of the month
141
+ # respectively. So each year has exactly 24 semimonths.
142
+ def beginning_of_semimonth
143
+ if day >= 16
144
+ ::Date.new(year, month, 16)
244
145
  else
245
- (today - 6.months).end_of_half
146
+ beginning_of_month
246
147
  end
247
- when /^(this_?)?year/
248
- if spec_type == :from
249
- today.beginning_of_year
148
+ end
149
+
150
+ # The date that is the last day of the semimonth in which self
151
+ # falls. A semimonth is a calendar period beginning on the 1st or
152
+ # 16th of each month and ending on the 15th or last day of the month
153
+ # respectively. So each year has exactly 24 semimonths.
154
+ def end_of_semimonth
155
+ if day <= 15
156
+ ::Date.new(year, month, 15)
250
157
  else
251
- today.end_of_year
158
+ end_of_month
252
159
  end
253
- when /^last_?year/
254
- if spec_type == :from
255
- (today - 1.year).beginning_of_year
160
+ end
161
+
162
+ # Note: we use a Monday start of the week in the next two methods because
163
+ # commercial week counting assumes a Monday start.
164
+ def beginning_of_biweek
165
+ if cweek.odd?
166
+ beginning_of_week(:monday)
256
167
  else
257
- (today - 1.year).end_of_year
168
+ (self - 1.week).beginning_of_week(:monday)
258
169
  end
259
- when /^forever/
260
- if spec_type == :from
261
- Date::BOT
170
+ end
171
+
172
+ def end_of_biweek
173
+ if cweek.odd?
174
+ (self + 1.week).end_of_week(:monday)
262
175
  else
263
- Date::EOT
176
+ end_of_week(:monday)
264
177
  end
265
- when /^never/
266
- nil
267
- else
268
- raise ArgumentError, "bad date spec: '#{spec}''"
269
- end # !> previous definition of length was here
270
- end
271
-
272
- # Predecessor of self. Allows Date to work as a countable element
273
- # of a Range.
274
- def pred
275
- self - 1.day
276
- end
277
-
278
- # Successor of self. Allows Date to work as a countable element
279
- # of a Range.
280
- def succ
281
- self + 1.day
282
- end
283
-
284
- # Format as an ISO string.
285
- def iso
286
- strftime('%Y-%m-%d')
287
- end
288
-
289
- # Format date to TeX documents as ISO strings
290
- def tex_quote
291
- iso
292
- end
293
-
294
- # Format as an all-numeric string, i.e. 'YYYYMMDD'
295
- def num
296
- strftime('%Y%m%d')
297
- end
298
-
299
- def format_by(fmt = '%Y-%m-%d')
300
- fmt ||= '%Y-%m-%d'
301
- strftime(fmt)
302
- end
303
-
304
- # Format as an inactive Org date (see emacs org-mode)
305
- def org
306
- strftime('[%Y-%m-%d %a]')
307
- end
308
-
309
- # Format as an English string
310
- def eng
311
- strftime('%B %e, %Y')
312
- end
313
-
314
- # Format date in MM/DD/YYYY form, as typical for the short American
315
- # form.
316
- def american
317
- strftime '%-m/%-d/%Y'
318
- end
319
-
320
- # Does self fall on a weekend?
321
- def weekend?
322
- saturday? || sunday?
323
- end
324
-
325
- # Does self fall on a weekday?
326
- def weekday?
327
- !weekend?
328
- end
329
-
330
- # Self's calendar half: 1 or 2
331
- def half
332
- case month
333
- when (1..6)
334
- 1
335
- when (7..12)
336
- 2
337
178
  end
338
- end
339
179
 
340
- # Self's calendar quarter: 1, 2, 3, or 4
341
- def quarter
342
- case month
343
- when (1..3)
344
- 1
345
- when (4..6)
346
- 2
347
- when (7..9)
348
- 3
349
- when (10..12)
350
- 4
180
+ def beginning_of_year?
181
+ beginning_of_year == self
351
182
  end
352
- end
353
183
 
354
- # The date that is the first day of the half-year in which self falls.
355
- def beginning_of_half
356
- if month > 9
357
- (beginning_of_quarter - 15).beginning_of_quarter
358
- elsif month > 6
359
- beginning_of_quarter
360
- else
361
- beginning_of_year
184
+ def end_of_year?
185
+ end_of_year == self
362
186
  end
363
- end
364
187
 
365
- # The date that is the last day of the half-year in which self falls.
366
- def end_of_half
367
- if month < 4
368
- (end_of_quarter + 15).end_of_quarter
369
- elsif month < 7
370
- end_of_quarter
371
- else
372
- end_of_year
188
+ def beginning_of_half?
189
+ beginning_of_half == self
373
190
  end
374
- end
375
191
 
376
- # The date that is the first day of the bimonth in which self
377
- # falls. A 'bimonth' is a two-month calendar period beginning on the
378
- # first day of the odd-numbered months. E.g., 2014-01-01 to
379
- # 2014-02-28 is the first bimonth of 2014.
380
- def beginning_of_bimonth
381
- if month.odd?
382
- beginning_of_month
383
- else
384
- (self - 1.month).beginning_of_month
192
+ def end_of_half?
193
+ end_of_half == self
385
194
  end
386
- end
387
195
 
388
- # The date that is the last day of the bimonth in which self falls.
389
- # A 'bimonth' is a two-month calendar period beginning on the first
390
- # day of the odd-numbered months. E.g., 2014-01-01 to 2014-02-28 is
391
- # the first bimonth of 2014.
392
- def end_of_bimonth
393
- if month.odd?
394
- (self + 1.month).end_of_month
395
- else
396
- end_of_month
196
+ def beginning_of_quarter?
197
+ beginning_of_quarter == self
397
198
  end
398
- end
399
199
 
400
- # The date that is the first day of the semimonth in which self
401
- # falls. A semimonth is a calendar period beginning on the 1st or
402
- # 16th of each month and ending on the 15th or last day of the month
403
- # respectively. So each year has exactly 24 semimonths.
404
- def beginning_of_semimonth
405
- if day >= 16
406
- Date.new(year, month, 16)
407
- else
408
- beginning_of_month
200
+ def end_of_quarter?
201
+ end_of_quarter == self
409
202
  end
410
- end
411
203
 
412
- # The date that is the last day of the semimonth in which self
413
- # falls. A semimonth is a calendar period beginning on the 1st or
414
- # 16th of each month and ending on the 15th or last day of the month
415
- # respectively. So each year has exactly 24 semimonths.
416
- def end_of_semimonth
417
- if day <= 15
418
- Date.new(year, month, 15)
419
- else
420
- end_of_month
204
+ def beginning_of_bimonth?
205
+ month.odd? && beginning_of_month == self
421
206
  end
422
- end
423
207
 
424
- # Note: we use a Monday start of the week in the next two methods because
425
- # commercial week counting assumes a Monday start.
426
- def beginning_of_biweek
427
- if cweek.odd?
428
- beginning_of_week(:monday)
429
- else
430
- (self - 1.week).beginning_of_week(:monday)
208
+ def end_of_bimonth?
209
+ month.even? && end_of_month == self
431
210
  end
432
- end
433
211
 
434
- def end_of_biweek
435
- if cweek.odd?
436
- (self + 1.week).end_of_week(:monday)
437
- else
438
- end_of_week(:monday)
212
+ def beginning_of_month?
213
+ beginning_of_month == self
439
214
  end
440
- end
441
-
442
- def beginning_of_year?
443
- beginning_of_year == self
444
- end
445
-
446
- def end_of_year?
447
- end_of_year == self
448
- end
449
-
450
- def beginning_of_half?
451
- beginning_of_half == self
452
- end
453
-
454
- def end_of_half?
455
- end_of_half == self
456
- end
457
-
458
- def beginning_of_quarter?
459
- beginning_of_quarter == self
460
- end
461
-
462
- def end_of_quarter?
463
- end_of_quarter == self
464
- end
465
-
466
- def beginning_of_bimonth?
467
- month.odd? && beginning_of_month == self
468
- end
469
-
470
- def end_of_bimonth?
471
- month.even? && end_of_month == self
472
- end
473
-
474
- def beginning_of_month?
475
- beginning_of_month == self
476
- end
477
-
478
- def end_of_month?
479
- end_of_month == self
480
- end
481
-
482
- def beginning_of_semimonth?
483
- beginning_of_semimonth == self
484
- end
485
215
 
486
- def end_of_semimonth?
487
- end_of_semimonth == self
488
- end
489
-
490
- def beginning_of_biweek?
491
- beginning_of_biweek == self
492
- end
493
-
494
- def end_of_biweek?
495
- end_of_biweek == self
496
- end
497
-
498
- def beginning_of_week?
499
- beginning_of_week == self
500
- end
216
+ def end_of_month?
217
+ end_of_month == self
218
+ end
501
219
 
502
- def end_of_week?
503
- end_of_week == self
504
- end
220
+ def beginning_of_semimonth?
221
+ beginning_of_semimonth == self
222
+ end
505
223
 
506
- def expand_to_period(sym)
507
- require 'fat_core/period'
508
- Period.new(beginning_of_chunk(sym), end_of_chunk(sym))
509
- end
224
+ def end_of_semimonth?
225
+ end_of_semimonth == self
226
+ end
510
227
 
511
- def add_chunk(chunk)
512
- case chunk
513
- when :year
514
- next_year
515
- when :half
516
- next_month(6)
517
- when :quarter
518
- next_month(3)
519
- when :bimonth
520
- next_month(2)
521
- when :month
522
- next_month
523
- when :semimonth
524
- self + 15.days
525
- when :biweek
526
- self + 14.days
527
- when :week
528
- self + 7.days
529
- when :day
530
- self + 1.days
531
- else
532
- raise ArgumentError, "add_chunk unknown chunk: '#{chunk}'"
228
+ def beginning_of_biweek?
229
+ beginning_of_biweek == self
533
230
  end
534
- end
535
231
 
536
- def beginning_of_chunk(sym)
537
- case sym
538
- when :year
539
- beginning_of_year
540
- when :half
541
- beginning_of_half
542
- when :quarter
543
- beginning_of_quarter
544
- when :bimonth
545
- beginning_of_bimonth
546
- when :month
547
- beginning_of_month
548
- when :semimonth
549
- beginning_of_semimonth
550
- when :biweek
551
- beginning_of_biweek
552
- when :week
553
- beginning_of_week
554
- when :day
555
- self
556
- else
557
- raise ArgumentError, "unknown chunk sym: '#{sym}'"
232
+ def end_of_biweek?
233
+ end_of_biweek == self
558
234
  end
559
- end
560
235
 
561
- def end_of_chunk(sym)
562
- case sym
563
- when :year
564
- end_of_year
565
- when :half
566
- end_of_half
567
- when :quarter
568
- end_of_quarter
569
- when :bimonth
570
- end_of_bimonth
571
- when :month
572
- end_of_month
573
- when :semimonth
574
- end_of_semimonth
575
- when :biweek
576
- end_of_biweek
577
- when :week
578
- end_of_week
579
- when :day
580
- self
581
- else
582
- raise ArgumentError, "unknown chunk sym: '#{sym}'"
236
+ def beginning_of_week?
237
+ beginning_of_week == self
583
238
  end
584
- end
585
239
 
586
- # Holidays decreed by executive order
587
- # See http://www.whitehouse.gov/the-press-office/2012/12/21/
588
- # executive-order-closing-executive-departments-and-agencies-federal-gover
589
- FED_DECREED_HOLIDAYS =
590
- [
591
- Date.parse('2012-12-24')
592
- ].freeze
593
-
594
- def self.days_in_month(y, m)
595
- raise ArgumentError, 'illegal month number' if m < 1 || m > 12
596
- days = Time::COMMON_YEAR_DAYS_IN_MONTH[m]
597
- if m == 2
598
- Date.new(y, m, 1).leap? ? 29 : 28
599
- else
600
- days
240
+ def end_of_week?
241
+ end_of_week == self
601
242
  end
602
- end
603
243
 
604
- def self.nth_wday_in_year_month(n, wday, year, month)
605
- # Return the nth weekday in the given month
606
- # If n is negative, count from last day of month
607
- wday = wday.to_i
608
- raise ArgumentError, 'illegal weekday number' if wday < 0 || wday > 6
609
- month = month.to_i
610
- raise ArgumentError, 'illegal month number' if month < 1 || month > 12
611
- n = n.to_i
612
- if n.positive?
613
- # Set d to the 1st wday in month
614
- d = Date.new(year, month, 1)
615
- d += 1 while d.wday != wday
616
- # Set d to the nth wday in month
617
- nd = 1
618
- while nd != n
619
- d += 7
620
- nd += 1
621
- end
622
- d
623
- elsif n.negative?
624
- n = -n
625
- # Set d to the last wday in month
626
- d = Date.new(year, month, 1).end_of_month
627
- d -= 1 while d.wday != wday
628
- # Set d to the nth wday in month
629
- nd = 1
630
- while nd != n
631
- d -= 7
632
- nd += 1
244
+ def add_chunk(chunk)
245
+ case chunk
246
+ when :year
247
+ next_year
248
+ when :half
249
+ next_month(6)
250
+ when :quarter
251
+ next_month(3)
252
+ when :bimonth
253
+ next_month(2)
254
+ when :month
255
+ next_month
256
+ when :semimonth
257
+ self + 15.days
258
+ when :biweek
259
+ self + 14.days
260
+ when :week
261
+ self + 7.days
262
+ when :day
263
+ self + 1.days
264
+ else
265
+ raise ArgumentError, "add_chunk unknown chunk: '#{chunk}'"
633
266
  end
634
- d
635
- else
636
- raise ArgumentError,
637
- 'Arg 1 to nth_wday_in_month_year cannot be zero'
638
267
  end
639
- end
640
-
641
- def within_6mos_of?(d)
642
- # Date 6 calendar months before self
643
- start_date = self - 6.months + 2.days
644
- end_date = self + 6.months - 2.days
645
- (start_date..end_date).cover?(d)
646
- end
647
268
 
648
- def self.easter(year)
649
- y = year
650
- a = y % 19
651
- b, c = y.divmod(100)
652
- d, e = b.divmod(4)
653
- f = (b + 8) / 25
654
- g = (b - f + 1) / 3
655
- h = (19 * a + b - d - g + 15) % 30
656
- i, k = c.divmod(4)
657
- l = (32 + 2 * e + 2 * i - h - k) % 7
658
- m = (a + 11 * h + 22 * l) / 451
659
- n, p = (h + l - 7 * m + 114).divmod(31)
660
- Date.new(y, n, p + 1)
661
- end
662
-
663
- def easter_this_year
664
- # Return the date of Easter in self's year
665
- Date.easter(year)
666
- end
269
+ def beginning_of_chunk(sym)
270
+ case sym
271
+ when :year
272
+ beginning_of_year
273
+ when :half
274
+ beginning_of_half
275
+ when :quarter
276
+ beginning_of_quarter
277
+ when :bimonth
278
+ beginning_of_bimonth
279
+ when :month
280
+ beginning_of_month
281
+ when :semimonth
282
+ beginning_of_semimonth
283
+ when :biweek
284
+ beginning_of_biweek
285
+ when :week
286
+ beginning_of_week
287
+ when :day
288
+ self
289
+ else
290
+ raise ArgumentError, "unknown chunk sym: '#{sym}'"
291
+ end
292
+ end
667
293
 
668
- def easter?
669
- # Am I Easter?
670
- self == easter_this_year
671
- end
294
+ def end_of_chunk(sym)
295
+ case sym
296
+ when :year
297
+ end_of_year
298
+ when :half
299
+ end_of_half
300
+ when :quarter
301
+ end_of_quarter
302
+ when :bimonth
303
+ end_of_bimonth
304
+ when :month
305
+ end_of_month
306
+ when :semimonth
307
+ end_of_semimonth
308
+ when :biweek
309
+ end_of_biweek
310
+ when :week
311
+ end_of_week
312
+ when :day
313
+ self
314
+ else
315
+ raise ArgumentError, "unknown chunk sym: '#{sym}'"
316
+ end
317
+ end
672
318
 
673
- def nth_wday_in_month?(n, wday, month)
674
- # Is self the nth weekday in the given month of its year?
675
- # If n is negative, count from last day of month
676
- self == Date.nth_wday_in_year_month(n, wday, year, month)
677
- end
319
+ def within_6mos_of?(d)
320
+ # Date 6 calendar months before self
321
+ start_date = self - 6.months + 2.days
322
+ end_date = self + 6.months - 2.days
323
+ (start_date..end_date).cover?(d)
324
+ end
678
325
 
679
- #######################################################
680
- # Calculations for Federal holidays
681
- # 5 USC 6103
682
- #######################################################
683
- def fed_fixed_holiday?
684
- # Fixed-date holidays on weekdays
685
- if mon == 1 && mday == 1
686
- # New Years (January 1),
687
- true
688
- elsif mon == 7 && mday == 4
689
- # Independence Day (July 4),
690
- true
691
- elsif mon == 11 && mday == 11
692
- # Veterans Day (November 11),
693
- true
694
- elsif mon == 12 && mday == 25
695
- # Christmas (December 25), and
696
- true
697
- else
698
- false
326
+ def easter_this_year
327
+ # Return the date of Easter in self's year
328
+ ::Date.easter(year)
699
329
  end
700
- end
701
330
 
702
- def fed_moveable_feast?
703
- # See if today is a "movable feast," all of which are
704
- # rigged to fall on Monday except Thanksgiving
705
-
706
- # No moveable feasts in certain months
707
- if [3, 4, 6, 7, 8, 12].include?(month)
708
- false
709
- elsif monday?
710
- moveable_mondays = []
711
- # MLK's Birthday (Third Monday in Jan)
712
- moveable_mondays << nth_wday_in_month?(3, 1, 1)
713
- # Washington's Birthday (Third Monday in Feb)
714
- moveable_mondays << nth_wday_in_month?(3, 1, 2)
715
- # Memorial Day (Last Monday in May)
716
- moveable_mondays << nth_wday_in_month?(-1, 1, 5)
717
- # Labor Day (First Monday in Sep)
718
- moveable_mondays << nth_wday_in_month?(1, 1, 9)
719
- # Columbus Day (Second Monday in Oct)
720
- moveable_mondays << nth_wday_in_month?(2, 1, 10)
721
- # Other Mondays
722
- moveable_mondays.any?
723
- elsif thursday?
724
- # Thanksgiving Day (Fourth Thur in Nov)
725
- nth_wday_in_month?(4, 4, 11)
726
- else
727
- false
331
+ def easter?
332
+ # Am I Easter?
333
+ self == easter_this_year
728
334
  end
729
- end
730
335
 
731
- def fed_holiday?
732
- # All Saturdays and Sundays are "holidays"
733
- return true if weekend?
734
-
735
- # Some days are holidays by executive decree
736
- return true if FED_DECREED_HOLIDAYS.include?(self)
737
-
738
- # Is self a fixed holiday
739
- return true if fed_fixed_holiday? || fed_moveable_feast?
740
-
741
- if friday? && month == 12 && day == 26
742
- # If Christmas falls on a Thursday, apparently, the Friday after is
743
- # treated as a holiday as well. See 2003, 2008, for example.
744
- true
745
- elsif friday?
746
- # A Friday is a holiday if a fixed-date holiday
747
- # would fall on the following Saturday
748
- (self + 1).fed_fixed_holiday? || (self + 1).fed_moveable_feast?
749
- elsif monday?
750
- # A Monday is a holiday if a fixed-date holiday
751
- # would fall on the preceding Sunday
752
- (self - 1).fed_fixed_holiday? || (self - 1).fed_moveable_feast?
753
- else
754
- false
336
+ def nth_wday_in_month?(n, wday, month)
337
+ # Is self the nth weekday in the given month of its year?
338
+ # If n is negative, count from last day of month
339
+ self == ::Date.nth_wday_in_year_month(n, wday, year, month)
755
340
  end
756
- end
757
341
 
758
- #######################################################
759
- # Calculations for NYSE holidays
760
- # Rule 51 and supplementary material
761
- #######################################################
762
-
763
- # Rule: if it falls on Saturday, observe on preceding Friday.
764
- # Rule: if it falls on Sunday, observe on following Monday.
765
- #
766
- # New Year's Day, January 1.
767
- # Birthday of Martin Luther King, Jr., the third Monday in January.
768
- # Washington's Birthday, the third Monday in February.
769
- # Good Friday Friday before Easter Sunday. NOTE: not a fed holiday
770
- # Memorial Day, the last Monday in May.
771
- # Independence Day, July 4.
772
- # Labor Day, the first Monday in September.
773
- # NOTE: Columbus and Veterans days not observed
774
- # Thanksgiving Day, the fourth Thursday in November.
775
- # Christmas Day, December 25.
776
-
777
- def nyse_fixed_holiday?
778
- # Fixed-date holidays
779
- if mon == 1 && mday == 1
780
- # New Years (January 1),
781
- true
782
- elsif mon == 7 && mday == 4
783
- # Independence Day (July 4),
784
- true
785
- elsif mon == 12 && mday == 25
786
- # Christmas (December 25), and
787
- true
788
- else
789
- false
342
+ #######################################################
343
+ # Calculations for Federal holidays
344
+ # 5 USC 6103
345
+ #######################################################
346
+ # Holidays decreed by executive order
347
+ # See http://www.whitehouse.gov/the-press-office/2012/12/21/
348
+ # executive-order-closing-executive-departments-and-agencies-federal-gover
349
+ FED_DECREED_HOLIDAYS =
350
+ [
351
+ ::Date.parse('2012-12-24')
352
+ ].freeze
353
+
354
+ def fed_fixed_holiday?
355
+ # Fixed-date holidays on weekdays
356
+ if mon == 1 && mday == 1
357
+ # New Years (January 1),
358
+ true
359
+ elsif mon == 7 && mday == 4
360
+ # Independence Day (July 4),
361
+ true
362
+ elsif mon == 11 && mday == 11
363
+ # Veterans Day (November 11),
364
+ true
365
+ elsif mon == 12 && mday == 25
366
+ # Christmas (December 25), and
367
+ true
368
+ else
369
+ false
370
+ end
790
371
  end
791
- end
792
372
 
793
- def nyse_moveable_feast?
794
- # See if today is a "movable feast," all of which are
795
- # rigged to fall on Monday except Thanksgiving
796
-
797
- # No moveable feasts in certain months
798
- return false if [6, 7, 8, 10, 12].include?(month)
799
-
800
- case month
801
- when 1
802
- # MLK's Birthday (Third Monday in Jan) since 1998
803
- year >= 1998 && nth_wday_in_month?(3, 1, 1)
804
- when 2
805
- # Lincoln's birthday until 1953
806
- # Washington's Birthday (Third Monday in Feb)
807
- (year <= 1953 && month == 2 && day == 12) ||
808
- (year <= 1970 ? (month == 2 && day == 22)
809
- : nth_wday_in_month?(3, 1, 2))
810
- when 3, 4
811
- # Good Friday
812
- if !friday?
373
+ def fed_moveable_feast?
374
+ # See if today is a "movable feast," all of which are
375
+ # rigged to fall on Monday except Thanksgiving
376
+
377
+ # No moveable feasts in certain months
378
+ if [3, 4, 6, 7, 8, 12].include?(month)
813
379
  false
814
- elsif [1898, 1906, 1907].include?(year)
815
- # Good Friday, the Friday before Easter, except certain years
380
+ elsif monday?
381
+ moveable_mondays = []
382
+ # MLK's Birthday (Third Monday in Jan)
383
+ moveable_mondays << nth_wday_in_month?(3, 1, 1)
384
+ # Washington's Birthday (Third Monday in Feb)
385
+ moveable_mondays << nth_wday_in_month?(3, 1, 2)
386
+ # Memorial Day (Last Monday in May)
387
+ moveable_mondays << nth_wday_in_month?(-1, 1, 5)
388
+ # Labor Day (First Monday in Sep)
389
+ moveable_mondays << nth_wday_in_month?(1, 1, 9)
390
+ # Columbus Day (Second Monday in Oct)
391
+ moveable_mondays << nth_wday_in_month?(2, 1, 10)
392
+ # Other Mondays
393
+ moveable_mondays.any?
394
+ elsif thursday?
395
+ # Thanksgiving Day (Fourth Thur in Nov)
396
+ nth_wday_in_month?(4, 4, 11)
397
+ else
816
398
  false
399
+ end
400
+ end
401
+
402
+ def fed_holiday?
403
+ # All Saturdays and Sundays are "holidays"
404
+ return true if weekend?
405
+
406
+ # Some days are holidays by executive decree
407
+ return true if FED_DECREED_HOLIDAYS.include?(self)
408
+
409
+ # Is self a fixed holiday
410
+ return true if fed_fixed_holiday? || fed_moveable_feast?
411
+
412
+ if friday? && month == 12 && day == 26
413
+ # If Christmas falls on a Thursday, apparently, the Friday after is
414
+ # treated as a holiday as well. See 2003, 2008, for example.
415
+ true
416
+ elsif friday?
417
+ # A Friday is a holiday if a fixed-date holiday
418
+ # would fall on the following Saturday
419
+ (self + 1).fed_fixed_holiday? || (self + 1).fed_moveable_feast?
420
+ elsif monday?
421
+ # A Monday is a holiday if a fixed-date holiday
422
+ # would fall on the preceding Sunday
423
+ (self - 1).fed_fixed_holiday? || (self - 1).fed_moveable_feast?
817
424
  else
818
- (self + 2).easter?
425
+ false
819
426
  end
820
- when 5
821
- # Memorial Day (Last Monday in May)
822
- year <= 1970 ? (month == 5 && day == 30) : nth_wday_in_month?(-1, 1, 5)
823
- when 9
824
- # Labor Day (First Monday in Sep)
825
- nth_wday_in_month?(1, 1, 9)
826
- when 10
827
- # Columbus Day (Oct 12) 1909--1953
828
- year >= 1909 && year <= 1953 && day == 12
829
- when 11
830
- if tuesday?
831
- # Election Day. Until 1968 all Election Days. From 1972 to 1980
832
- # Election Day in presidential years only. Election Day is the first
833
- # Tuesday after the first Monday in November.
834
- first_tuesday = Date.nth_wday_in_year_month(1, 1, year, 11) + 1
835
- is_election_day = (self == first_tuesday)
836
- if year <= 1968
837
- is_election_day
838
- elsif year <= 1980
839
- is_election_day && (year % 4).zero?
840
- else
427
+ end
428
+
429
+ #######################################################
430
+ # Calculations for NYSE holidays
431
+ # Rule 51 and supplementary material
432
+ #######################################################
433
+
434
+ # Rule: if it falls on Saturday, observe on preceding Friday.
435
+ # Rule: if it falls on Sunday, observe on following Monday.
436
+ #
437
+ # New Year's Day, January 1.
438
+ # Birthday of Martin Luther King, Jr., the third Monday in January.
439
+ # Washington's Birthday, the third Monday in February.
440
+ # Good Friday Friday before Easter Sunday. NOTE: not a fed holiday
441
+ # Memorial Day, the last Monday in May.
442
+ # Independence Day, July 4.
443
+ # Labor Day, the first Monday in September.
444
+ # NOTE: Columbus and Veterans days not observed
445
+ # Thanksgiving Day, the fourth Thursday in November.
446
+ # Christmas Day, December 25.
447
+
448
+ def nyse_fixed_holiday?
449
+ # Fixed-date holidays
450
+ if mon == 1 && mday == 1
451
+ # New Years (January 1),
452
+ true
453
+ elsif mon == 7 && mday == 4
454
+ # Independence Day (July 4),
455
+ true
456
+ elsif mon == 12 && mday == 25
457
+ # Christmas (December 25), and
458
+ true
459
+ else
460
+ false
461
+ end
462
+ end
463
+
464
+ def nyse_moveable_feast?
465
+ # See if today is a "movable feast," all of which are
466
+ # rigged to fall on Monday except Thanksgiving
467
+
468
+ # No moveable feasts in certain months
469
+ return false if [6, 7, 8, 10, 12].include?(month)
470
+
471
+ case month
472
+ when 1
473
+ # MLK's Birthday (Third Monday in Jan) since 1998
474
+ year >= 1998 && nth_wday_in_month?(3, 1, 1)
475
+ when 2
476
+ # Lincoln's birthday until 1953
477
+ # Washington's Birthday (Third Monday in Feb)
478
+ (year <= 1953 && month == 2 && day == 12) ||
479
+ (year <= 1970 ? (month == 2 && day == 22)
480
+ : nth_wday_in_month?(3, 1, 2))
481
+ when 3, 4
482
+ # Good Friday
483
+ if !friday?
841
484
  false
485
+ elsif [1898, 1906, 1907].include?(year)
486
+ # Good Friday, the Friday before Easter, except certain years
487
+ false
488
+ else
489
+ (self + 2).easter?
842
490
  end
843
- elsif thursday?
844
- # Historically Thanksgiving (NYSE closed all day) had been declared to
845
- # be the last Thursday in November until 1938; the next-to-last
846
- # Thursday in November from 1939 to 1941 (therefore the 3rd Thursday
847
- # in 1940 and 1941); the last Thursday in November in 1942; the fourth
848
- # Thursday in November since 1943;
849
- if year < 1938
850
- nth_wday_in_month?(-1, 4, 11)
851
- elsif year <= 1941
852
- nth_wday_in_month?(3, 4, 11)
853
- elsif year == 1942
854
- nth_wday_in_month?(-1, 4, 11)
491
+ when 5
492
+ # Memorial Day (Last Monday in May)
493
+ year <= 1970 ? (month == 5 && day == 30) : nth_wday_in_month?(-1, 1, 5)
494
+ when 9
495
+ # Labor Day (First Monday in Sep)
496
+ nth_wday_in_month?(1, 1, 9)
497
+ when 10
498
+ # Columbus Day (Oct 12) 1909--1953
499
+ year >= 1909 && year <= 1953 && day == 12
500
+ when 11
501
+ if tuesday?
502
+ # Election Day. Until 1968 all Election Days. From 1972 to 1980
503
+ # Election Day in presidential years only. Election Day is the first
504
+ # Tuesday after the first Monday in November.
505
+ first_tuesday = ::Date.nth_wday_in_year_month(1, 1, year, 11) + 1
506
+ is_election_day = (self == first_tuesday)
507
+ if year <= 1968
508
+ is_election_day
509
+ elsif year <= 1980
510
+ is_election_day && (year % 4).zero?
511
+ else
512
+ false
513
+ end
514
+ elsif thursday?
515
+ # Historically Thanksgiving (NYSE closed all day) had been declared to
516
+ # be the last Thursday in November until 1938; the next-to-last
517
+ # Thursday in November from 1939 to 1941 (therefore the 3rd Thursday
518
+ # in 1940 and 1941); the last Thursday in November in 1942; the fourth
519
+ # Thursday in November since 1943;
520
+ if year < 1938
521
+ nth_wday_in_month?(-1, 4, 11)
522
+ elsif year <= 1941
523
+ nth_wday_in_month?(3, 4, 11)
524
+ elsif year == 1942
525
+ nth_wday_in_month?(-1, 4, 11)
526
+ else
527
+ nth_wday_in_month?(4, 4, 11)
528
+ end
529
+ elsif day == 11
530
+ # Armistice or Veterans Day. 1918--1921; 1934--1953.
531
+ (year >= 1918 && year <= 1921) || (year >= 1934 && year <= 1953)
855
532
  else
856
- nth_wday_in_month?(4, 4, 11)
533
+ false
857
534
  end
858
- elsif day == 11
859
- # Armistice or Veterans Day. 1918--1921; 1934--1953.
860
- (year >= 1918 && year <= 1921) || (year >= 1934 && year <= 1953)
861
535
  else
862
536
  false
863
537
  end
864
- else
865
- false
866
538
  end
867
- end
868
539
 
869
- # They NYSE has closed on several occasions outside its normal holiday
870
- # rules. This detects those dates beginning in 1960. Closing for part of a
871
- # day is not counted. See http://www1.nyse.com/pdfs/closings.pdf
872
- def nyse_special_holiday?
873
- return false unless self > Date.parse('1960-01-01')
874
- case self
875
- when Date.parse('1961-05-29')
876
- # Day before Decoaration Day
877
- true
878
- when Date.parse('1963-11-25')
879
- # President Kennedy's funeral
880
- true
881
- when Date.parse('1965-12-24')
882
- # Christmas eve unscheduled for normal holiday
883
- true
884
- when Date.parse('1968-02-12')
885
- # Lincoln birthday
886
- true
887
- when Date.parse('1968-04-09')
888
- # Mourning MLK
889
- true
890
- when Date.parse('1968-07-05')
891
- # Day after Independence Day
892
- true
893
- when (Date.parse('1968-06-12')..Date.parse('1968-12-31'))
894
- # Paperwork crisis (closed on Wednesdays if no other holiday in week)
895
- wednesday? && (self - 2).nyse_workday? && (self - 1).nyse_workday? &&
896
- (self + 1).nyse_workday? && (self + 2).nyse_workday?
897
- when Date.parse('1969-02-10')
898
- # Heavy snow
899
- true
900
- when Date.parse('1969-05-31')
901
- # Eisenhower Funeral
902
- true
903
- when Date.parse('1969-07-21')
904
- # Moon landing
905
- true
906
- when Date.parse('1972-12-28')
907
- # Truman Funeral
908
- true
909
- when Date.parse('1973-01-25')
910
- # Johnson Funeral
911
- true
912
- when Date.parse('1977-07-14')
913
- # Electrical blackout NYC
914
- true
915
- when Date.parse('1985-09-27')
916
- # Hurricane Gloria
917
- true
918
- when Date.parse('1994-04-27')
919
- # Nixon Funeral
920
- true
921
- when (Date.parse('2001-09-11')..Date.parse('2001-09-14'))
922
- # 9-11 Attacks
923
- true
924
- when (Date.parse('2004-06-11')..Date.parse('2001-09-14'))
925
- # Reagan Funeral
926
- true
927
- when Date.parse('2007-01-02')
928
- # Observance death of President Ford
929
- true
930
- when Date.parse('2012-10-29'), Date.parse('2012-10-30')
931
- # Hurricane Sandy
932
- true
933
- else
934
- false
540
+ # They NYSE has closed on several occasions outside its normal holiday
541
+ # rules. This detects those dates beginning in 1960. Closing for part of a
542
+ # day is not counted. See http://www1.nyse.com/pdfs/closings.pdf
543
+ def nyse_special_holiday?
544
+ return false unless self > ::Date.parse('1960-01-01')
545
+ case self
546
+ when ::Date.parse('1961-05-29')
547
+ # Day before Decoaration Day
548
+ true
549
+ when ::Date.parse('1963-11-25')
550
+ # President Kennedy's funeral
551
+ true
552
+ when ::Date.parse('1965-12-24')
553
+ # Christmas eve unscheduled for normal holiday
554
+ true
555
+ when ::Date.parse('1968-02-12')
556
+ # Lincoln birthday
557
+ true
558
+ when ::Date.parse('1968-04-09')
559
+ # Mourning MLK
560
+ true
561
+ when ::Date.parse('1968-07-05')
562
+ # Day after Independence Day
563
+ true
564
+ when (::Date.parse('1968-06-12')..::Date.parse('1968-12-31'))
565
+ # Paperwork crisis (closed on Wednesdays if no other holiday in week)
566
+ wednesday? && (self - 2).nyse_workday? && (self - 1).nyse_workday? &&
567
+ (self + 1).nyse_workday? && (self + 2).nyse_workday?
568
+ when ::Date.parse('1969-02-10')
569
+ # Heavy snow
570
+ true
571
+ when ::Date.parse('1969-05-31')
572
+ # Eisenhower Funeral
573
+ true
574
+ when ::Date.parse('1969-07-21')
575
+ # Moon landing
576
+ true
577
+ when ::Date.parse('1972-12-28')
578
+ # Truman Funeral
579
+ true
580
+ when ::Date.parse('1973-01-25')
581
+ # Johnson Funeral
582
+ true
583
+ when ::Date.parse('1977-07-14')
584
+ # Electrical blackout NYC
585
+ true
586
+ when ::Date.parse('1985-09-27')
587
+ # Hurricane Gloria
588
+ true
589
+ when ::Date.parse('1994-04-27')
590
+ # Nixon Funeral
591
+ true
592
+ when (::Date.parse('2001-09-11')..::Date.parse('2001-09-14'))
593
+ # 9-11 Attacks
594
+ a = a
595
+ true
596
+ when (::Date.parse('2004-06-11')..::Date.parse('2001-09-14'))
597
+ # Reagan Funeral
598
+ true
599
+ when ::Date.parse('2007-01-02')
600
+ # Observance death of President Ford
601
+ true
602
+ when ::Date.parse('2012-10-29'), ::Date.parse('2012-10-30')
603
+ # Hurricane Sandy
604
+ true
605
+ else
606
+ false
607
+ end
608
+ end
609
+
610
+ def nyse_holiday?
611
+ # All Saturdays and Sundays are "holidays"
612
+ return true if weekend?
613
+
614
+ # Is self a fixed holiday
615
+ return true if nyse_fixed_holiday? || nyse_moveable_feast?
616
+
617
+ return true if nyse_special_holiday?
618
+
619
+ if friday? && (self >= ::Date.parse('1959-07-03'))
620
+ # A Friday is a holiday if a holiday would fall on the following
621
+ # Saturday. The rule does not apply if the Friday "ends a monthly or
622
+ # yearly accounting period." Adopted July 3, 1959. E.g, December 31,
623
+ # 2010, fell on a Friday, so New Years was on Saturday, but the NYSE
624
+ # opened because it ended a yearly accounting period. I believe 12/31
625
+ # is the only date to which the exception can apply since only New
626
+ # Year's can fall on the first of the month.
627
+ !end_of_quarter? &&
628
+ ((self + 1).nyse_fixed_holiday? || (self + 1).nyse_moveable_feast?)
629
+ elsif monday?
630
+ # A Monday is a holiday if a holiday would fall on the
631
+ # preceding Sunday. This has apparently always been the rule.
632
+ (self - 1).nyse_fixed_holiday? || (self - 1).nyse_moveable_feast?
633
+ else
634
+ false
635
+ end
935
636
  end
936
- end
937
637
 
938
- def nyse_holiday?
939
- # All Saturdays and Sundays are "holidays"
940
- return true if weekend?
941
-
942
- # Is self a fixed holiday
943
- return true if nyse_fixed_holiday? || nyse_moveable_feast?
944
-
945
- return true if nyse_special_holiday?
946
-
947
- if friday? && (self >= Date.parse('1959-07-03'))
948
- # A Friday is a holiday if a holiday would fall on the following
949
- # Saturday. The rule does not apply if the Friday "ends a monthly or
950
- # yearly accounting period." Adopted July 3, 1959. E.g, December 31,
951
- # 2010, fell on a Friday, so New Years was on Saturday, but the NYSE
952
- # opened because it ended a yearly accounting period. I believe 12/31
953
- # is the only date to which the exception can apply since only New
954
- # Year's can fall on the first of the month.
955
- !end_of_quarter? &&
956
- ((self + 1).nyse_fixed_holiday? || (self + 1).nyse_moveable_feast?)
957
- elsif monday?
958
- # A Monday is a holiday if a holiday would fall on the
959
- # preceding Sunday. This has apparently always been the rule.
960
- (self - 1).nyse_fixed_holiday? || (self - 1).nyse_moveable_feast?
961
- else
962
- false
638
+ def fed_workday?
639
+ !fed_holiday?
963
640
  end
964
- end
965
641
 
966
- def fed_workday?
967
- !fed_holiday?
968
- end
642
+ def nyse_workday?
643
+ !nyse_holiday?
644
+ end
645
+ alias trading_day? nyse_workday?
646
+
647
+ def add_fed_business_days(n)
648
+ d = dup
649
+ return d if n.zero?
650
+ incr = n.negative? ? -1 : 1
651
+ n = n.abs
652
+ while n.positive?
653
+ d += incr
654
+ n -= 1 if d.fed_workday?
655
+ end
656
+ d
657
+ end
969
658
 
970
- def nyse_workday?
971
- !nyse_holiday?
972
- end
973
- alias trading_day? nyse_workday?
974
-
975
- def add_fed_business_days(n)
976
- d = dup
977
- return d if n.zero?
978
- incr = n.negative? ? -1 : 1
979
- n = n.abs
980
- while n.positive?
981
- d += incr
982
- n -= 1 if d.fed_workday?
659
+ def next_fed_workday
660
+ add_fed_business_days(1)
983
661
  end
984
- d
985
- end
986
662
 
987
- def next_fed_workday
988
- add_fed_business_days(1)
989
- end
663
+ def prior_fed_workday
664
+ add_fed_business_days(-1)
665
+ end
990
666
 
991
- def prior_fed_workday
992
- add_fed_business_days(-1)
993
- end
667
+ def add_nyse_business_days(n)
668
+ d = dup
669
+ return d if n.zero?
670
+ incr = n.negative? ? -1 : 1
671
+ n = n.abs
672
+ while n.positive?
673
+ d += incr
674
+ n -= 1 if d.nyse_workday?
675
+ end
676
+ d
677
+ end
678
+ alias add_trading_days add_nyse_business_days
994
679
 
995
- def add_nyse_business_days(n)
996
- d = dup
997
- return d if n.zero?
998
- incr = n.negative? ? -1 : 1
999
- n = n.abs
1000
- while n.positive?
1001
- d += incr
1002
- n -= 1 if d.nyse_workday?
680
+ def next_nyse_workday
681
+ add_nyse_business_days(1)
1003
682
  end
1004
- d
1005
- end
1006
- alias add_trading_days add_nyse_business_days
683
+ alias next_trading_day next_nyse_workday
1007
684
 
1008
- def next_nyse_workday
1009
- add_nyse_business_days(1)
1010
- end
1011
- alias next_trading_day next_nyse_workday
685
+ def prior_nyse_workday
686
+ add_nyse_business_days(-1)
687
+ end
688
+ alias prior_trading_day prior_nyse_workday
689
+
690
+ # Return self if its a trading day, otherwise skip back to the first prior
691
+ # trading day.
692
+ def prior_until_trading_day
693
+ date = dup
694
+ date -= 1 until date.trading_day?
695
+ date
696
+ end
1012
697
 
1013
- def prior_nyse_workday
1014
- add_nyse_business_days(-1)
1015
- end
1016
- alias prior_trading_day prior_nyse_workday
1017
-
1018
- # Return self if its a trading day, otherwise skip back to the first prior
1019
- # trading day.
1020
- def prior_until_trading_day
1021
- date = dup
1022
- date -= 1 until date.trading_day?
1023
- date
1024
- end
698
+ # Return self if its a trading day, otherwise skip forward to the first
699
+ # later trading day.
700
+ def next_until_trading_day
701
+ date = dup
702
+ date += 1 until date.trading_day?
703
+ date
704
+ end
705
+
706
+ module ClassMethods
707
+ # Convert a string with an American style date into a Date object
708
+ #
709
+ # An American style date is of the form MM/DD/YYYY, that is it places the
710
+ # month first, then the day of the month, and finally the year. The
711
+ # European convention is to place the day of the month first, DD/MM/YYYY.
712
+ # Because a date found in the wild can be ambiguous, e.g. 3/5/2014, a date
713
+ # string known to be using the American convention can be parsed using this
714
+ # method. Both the month and the day can be a single digit. The year can
715
+ # be either 2 or 4 digits, and if given as 2 digits, it adds 2000 to it to
716
+ # give the year.
717
+ #
718
+ # @example
719
+ # Date.parse_american('9/11/2001') #=> Date(2011, 9, 11)
720
+ # Date.parse_american('9/11/01') #=> Date(2011, 9, 11)
721
+ # Date.parse_american('9/11/1') #=> ArgumentError
722
+ #
723
+ # @param str [#to_s] a stringling of the form MM/DD/YYYY
724
+ # @return [Date] the date represented by the string paramenter.
725
+ def parse_american(str)
726
+ unless str.to_s =~ %r{\A\s*(\d\d?)\s*/\s*(\d\d?)\s*/\s*(\d?\d?\d\d)\s*\z}
727
+ raise ArgumentError, "date string must be of form 'MM?/DD?/YY(YY)?'"
728
+ end
729
+ year = $3.to_i
730
+ month = $1.to_i
731
+ day = $2.to_i
732
+ year += 2000 if year < 100
733
+ ::Date.new(year, month, day)
734
+ end
735
+
736
+ # Convert a 'date spec' to a Date. A date spec is a short-hand way of
737
+ # specifying a date, relative to the computer clock. A date spec can
738
+ # interpreted as either a 'from spec' or a 'to spec'.
739
+ # @example
740
+ #
741
+ # Assuming that Date.current at the time of execution is 2014-07-26 and
742
+ # using the default spec_type of :from. The return values are actually Date
743
+ # objects, but are shown below as textual dates.
744
+ #
745
+ # A fully specified date returns that date:
746
+ # Date.parse_spec('2001-09-11') # =>
747
+ # Commercial weeks can be specified using, for example W32 or 32W, with the
748
+ # week beginning on Monday, ending on Sunday.
749
+ # Date.parse_spec('2012-W32') # =>
750
+ # Date.parse_spec('2012-W32', :to) # =>
751
+ # Date.parse_spec('W32') # =>
752
+ #
753
+ # A spec of the form Q3 or 3Q returns the beginning or end of calendar
754
+ # quarters.
755
+ # Date.parse_spec('Q3') # =>
756
+ #
757
+ # @param spec [#to_s] a stringling containing the spec to be interpreted
758
+ # @param spec_type [:from, :to] interpret the spec as a from- or to-spec
759
+ # respectively, defaulting to interpretation as a to-spec.
760
+ # @return [Date] a date object equivalent to the date spec
761
+ def parse_spec(spec, spec_type = :from)
762
+ spec = spec.to_s.strip
763
+ unless [:from, :to].include?(spec_type)
764
+ raise ArgumentError, "invalid date spec type: '#{spec_type}'"
765
+ end
766
+
767
+ today = ::Date.current
768
+ case spec.clean
769
+ when /\A(\d\d\d\d)[-\/](\d\d?)[-\/](\d\d?)\z/
770
+ # A specified date
771
+ ::Date.new($1.to_i, $2.to_i, $3.to_i)
772
+ when /\AW(\d\d?)\z/, /\A(\d\d?)W\z/
773
+ week_num = $1.to_i
774
+ if week_num < 1 || week_num > 53
775
+ raise ArgumentError, "invalid week number (1-53): 'W#{week_num}'"
776
+ end
777
+ if spec_type == :from
778
+ ::Date.commercial(today.year, week_num).beginning_of_week
779
+ else
780
+ ::Date.commercial(today.year, week_num).end_of_week
781
+ end
782
+ when /\A(\d\d\d\d)[-\/]W(\d\d?)\z/, /\A(\d\d\d\d)[-\/](\d\d?)W\z/
783
+ year = $1.to_i
784
+ week_num = $2.to_i
785
+ if week_num < 1 || week_num > 53
786
+ raise ArgumentError, "invalid week number (1-53): 'W#{week_num}'"
787
+ end
788
+ if spec_type == :from
789
+ ::Date.commercial(year, week_num).beginning_of_week
790
+ else
791
+ ::Date.commercial(year, week_num).end_of_week
792
+ end
793
+ when /^(\d\d\d\d)[-\/](\d)[Qq]$/, /^(\d\d\d\d)[-\/][Qq](\d)$/
794
+ # Year-Quarter
795
+ year = $1.to_i
796
+ quarter = $2.to_i
797
+ unless [1, 2, 3, 4].include?(quarter)
798
+ raise ArgumentError, "bad date format: #{spec}"
799
+ end
800
+ month = quarter * 3
801
+ if spec_type == :from
802
+ ::Date.new(year, month, 1).beginning_of_quarter
803
+ else
804
+ ::Date.new(year, month, 1).end_of_quarter
805
+ end
806
+ when /^([1234])[qQ]$/, /^[qQ]([1234])$/
807
+ # Quarter only
808
+ this_year = today.year
809
+ quarter = $1.to_i
810
+ date = ::Date.new(this_year, quarter * 3, 15)
811
+ if spec_type == :from
812
+ date.beginning_of_quarter
813
+ else
814
+ date.end_of_quarter
815
+ end
816
+ when /^(\d\d\d\d)[-\/](\d)[Hh]$/, /^(\d\d\d\d)[-\/][Hh](\d)$/
817
+ # Year-Half
818
+ year = $1.to_i
819
+ half = $2.to_i
820
+ unless [1, 2].include?(half)
821
+ raise ArgumentError, "bad date format: #{spec}"
822
+ end
823
+ month = half * 6
824
+ if spec_type == :from
825
+ ::Date.new(year, month, 15).beginning_of_half
826
+ else
827
+ ::Date.new(year, month, 1).end_of_half
828
+ end
829
+ when /^([12])[hH]$/, /^[hH]([12])$/
830
+ # Half only
831
+ this_year = today.year
832
+ half = $1.to_i
833
+ date = ::Date.new(this_year, half * 6, 15)
834
+ if spec_type == :from
835
+ date.beginning_of_half
836
+ else
837
+ date.end_of_half
838
+ end
839
+ when /^(\d\d\d\d)[-\/](\d\d?)*$/
840
+ # Year-Month only
841
+ if spec_type == :from
842
+ ::Date.new($1.to_i, $2.to_i, 1)
843
+ else
844
+ ::Date.new($1.to_i, $2.to_i, 1).end_of_month
845
+ end
846
+ when /^(\d\d?)[-\/](\d\d?)*$/
847
+ # Month-Day only
848
+ if spec_type == :from
849
+ ::Date.new(today.year, $1.to_i, $2.to_i)
850
+ else
851
+ ::Date.new(today.year, $1.to_i, $2.to_i).end_of_month
852
+ end
853
+ when /\A(\d\d?)\z/
854
+ # Month only
855
+ if spec_type == :from
856
+ ::Date.new(today.year, $1.to_i, 1)
857
+ else
858
+ ::Date.new(today.year, $1.to_i, 1).end_of_month
859
+ end
860
+ when /^(\d\d\d\d)$/
861
+ # Year only
862
+ if spec_type == :from
863
+ ::Date.new($1.to_i, 1, 1)
864
+ else
865
+ ::Date.new($1.to_i, 12, 31)
866
+ end
867
+ when /^(to|this_?)?day/
868
+ today
869
+ when /^(yester|last_?)?day/
870
+ today - 1.day
871
+ when /^(this_?)?week/
872
+ spec_type == :from ? today.beginning_of_week : today.end_of_week
873
+ when /last_?week/
874
+ if spec_type == :from
875
+ (today - 1.week).beginning_of_week
876
+ else
877
+ (today - 1.week).end_of_week
878
+ end
879
+ when /^(this_?)?biweek/
880
+ if spec_type == :from
881
+ today.beginning_of_biweek
882
+ else
883
+ today.end_of_biweek
884
+ end
885
+ when /last_?biweek/
886
+ if spec_type == :from
887
+ (today - 2.week).beginning_of_biweek
888
+ else
889
+ (today - 2.week).end_of_biweek
890
+ end
891
+ when /^(this_?)?semimonth/
892
+ spec_type == :from ? today.beginning_of_semimonth : today.end_of_semimonth
893
+ when /^last_?semimonth/
894
+ if spec_type == :from
895
+ (today - 15.days).beginning_of_semimonth
896
+ else
897
+ (today - 15.days).end_of_semimonth
898
+ end
899
+ when /^(this_?)?month/
900
+ if spec_type == :from
901
+ today.beginning_of_month
902
+ else
903
+ today.end_of_month
904
+ end
905
+ when /^last_?month/
906
+ if spec_type == :from
907
+ (today - 1.month).beginning_of_month
908
+ else
909
+ (today - 1.month).end_of_month
910
+ end
911
+ when /^(this_?)?bimonth/
912
+ if spec_type == :from
913
+ today.beginning_of_bimonth
914
+ else
915
+ today.end_of_bimonth
916
+ end
917
+ when /^last_?bimonth/
918
+ if spec_type == :from
919
+ (today - 2.month).beginning_of_bimonth
920
+ else
921
+ (today - 2.month).end_of_bimonth
922
+ end
923
+ when /^(this_?)?quarter/
924
+ if spec_type == :from
925
+ today.beginning_of_quarter
926
+ else
927
+ today.end_of_quarter
928
+ end
929
+ when /^last_?quarter/
930
+ if spec_type == :from
931
+ (today - 3.months).beginning_of_quarter
932
+ else
933
+ (today - 3.months).end_of_quarter
934
+ end
935
+ when /^(this_?)?half/
936
+ if spec_type == :from
937
+ today.beginning_of_half
938
+ else
939
+ today.end_of_half
940
+ end
941
+ when /^last_?half/
942
+ if spec_type == :from
943
+ (today - 6.months).beginning_of_half
944
+ else
945
+ (today - 6.months).end_of_half
946
+ end
947
+ when /^(this_?)?year/
948
+ if spec_type == :from
949
+ today.beginning_of_year
950
+ else
951
+ today.end_of_year
952
+ end
953
+ when /^last_?year/
954
+ if spec_type == :from
955
+ (today - 1.year).beginning_of_year
956
+ else
957
+ (today - 1.year).end_of_year
958
+ end
959
+ when /^forever/
960
+ if spec_type == :from
961
+ ::Date::BOT
962
+ else
963
+ ::Date::EOT
964
+ end
965
+ when /^never/
966
+ nil
967
+ else
968
+ raise ArgumentError, "bad date spec: '#{spec}''"
969
+ end # !> previous definition of length was here
970
+ end
971
+
972
+ COMMON_YEAR_DAYS_IN_MONTH = [31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31,
973
+ 30, 31]
974
+ def days_in_month(y, m)
975
+ raise ArgumentError, 'illegal month number' if m < 1 || m > 12
976
+ days = COMMON_YEAR_DAYS_IN_MONTH[m]
977
+ if m == 2
978
+ ::Date.new(y, m, 1).leap? ? 29 : 28
979
+ else
980
+ days
981
+ end
982
+ end
1025
983
 
1026
- # Return self if its a trading day, otherwise skip forward to the first
1027
- # later trading day.
1028
- def next_until_trading_day
1029
- date = dup
1030
- date += 1 until date.trading_day?
1031
- date
984
+ def nth_wday_in_year_month(n, wday, year, month)
985
+ # Return the nth weekday in the given month
986
+ # If n is negative, count from last day of month
987
+ wday = wday.to_i
988
+ raise ArgumentError, 'illegal weekday number' if wday < 0 || wday > 6
989
+ month = month.to_i
990
+ raise ArgumentError, 'illegal month number' if month < 1 || month > 12
991
+ n = n.to_i
992
+ if n.positive?
993
+ # Set d to the 1st wday in month
994
+ d = ::Date.new(year, month, 1)
995
+ d += 1 while d.wday != wday
996
+ # Set d to the nth wday in month
997
+ nd = 1
998
+ while nd != n
999
+ d += 7
1000
+ nd += 1
1001
+ end
1002
+ d
1003
+ elsif n.negative?
1004
+ n = -n
1005
+ # Set d to the last wday in month
1006
+ d = ::Date.new(year, month, 1).end_of_month
1007
+ d -= 1 while d.wday != wday
1008
+ # Set d to the nth wday in month
1009
+ nd = 1
1010
+ while nd != n
1011
+ d -= 7
1012
+ nd += 1
1013
+ end
1014
+ d
1015
+ else
1016
+ raise ArgumentError,
1017
+ 'Arg 1 to nth_wday_in_month_year cannot be zero'
1018
+ end
1019
+ end
1020
+
1021
+ def easter(year)
1022
+ y = year
1023
+ a = y % 19
1024
+ b, c = y.divmod(100)
1025
+ d, e = b.divmod(4)
1026
+ f = (b + 8) / 25
1027
+ g = (b - f + 1) / 3
1028
+ h = (19 * a + b - d - g + 15) % 30
1029
+ i, k = c.divmod(4)
1030
+ l = (32 + 2 * e + 2 * i - h - k) % 7
1031
+ m = (a + 11 * h + 22 * l) / 451
1032
+ n, p = (h + l - 7 * m + 114).divmod(31)
1033
+ ::Date.new(y, n, p + 1)
1034
+ end
1035
+ end
1036
+
1037
+ # This hook gets called by the host class when it includes this
1038
+ # module, extending that class to include the methods defined in
1039
+ # ClassMethods as class methods of the host class.
1040
+ def self.included(host_class)
1041
+ host_class.extend(ClassMethods)
1042
+ end
1032
1043
  end
1033
1044
  end
1045
+
1046
+ Date.include(FatCore::Date)