fat_core 2.0.1 → 3.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.
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)