fat_core 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3607f0be1274b7aaf76f304d49f3520e77d660a9
4
+ data.tar.gz: ad4c5ae566a070959c19c5bd6dab95c43ed0f234
5
+ SHA512:
6
+ metadata.gz: f59a2e0bfa35cf755a4578a5daf84e614c9f11470c1fe3f94d6cfc1edf79cb4ab5a054813bc961f74edb82a0e76daee8d92e01bd0a1c5ff041b5cf5d216a916d
7
+ data.tar.gz: d3fecd8a83aec2101df3f1bac6d6427773c71e73a6158b6ab456330ca5598cc44710bef01ae863d3bc787ebed4a73f41ebc9402c15d4b42e0486a71a702e2e6a
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --no-private --protected lib/**/*.rb - README LICENSE
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fat_core.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Daniel E. Doherty
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # FatCore
2
+
3
+ fat-core is a simple gem to collect core extensions and a few new classes that
4
+ I find useful in multiple projects. The emphasis is on extending the Date
5
+ class to make it more useful in financial applications.
6
+
7
+ For example, the Date class adds two methods for determining whether a given
8
+ date is a US federal holiday or a NYSE holiday.
9
+
10
+ Date.parse('2014-05-18').fed_holiday? => true # It's a weekend
11
+ Date.parse('2014-01-01').fed_holiday? => true # It's New Years
12
+
13
+ All holidays defined by federal statute are recognized.
14
+
15
+ Likewise, days on which the NYSE is closed can be gotten with:
16
+
17
+ Date.parse('2014-04-18').nyse_holiday? => true # It's Good Friday
18
+
19
+ Conversely, Date#fed_workday? and Date#nyse_workday? return true if they are
20
+ open for business on those days.
21
+
22
+ ## Installation
23
+
24
+ Add this line to your application's Gemfile:
25
+
26
+ gem 'fat_core', :git => 'https://github.com/ddoherty03/fat_core.git'
27
+
28
+ And then execute:
29
+
30
+ $ bundle
31
+
32
+ Or install it yourself as:
33
+
34
+ $ gem install fat_core
35
+
36
+ ## Usage
37
+
38
+ TODO: Write usage instructions here
39
+
40
+ ## Contributing
41
+
42
+ 1. Fork it ( http://github.com/<my-github-username>/fat_core/fork )
43
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
44
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
45
+ 4. Push to the branch (`git push origin my-new-feature`)
46
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/fat_core.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'fat_core/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "fat_core"
8
+ spec.version = FatCore::VERSION
9
+ spec.authors = ["Daniel E. Doherty"]
10
+ spec.email = ["ded-law@ddoherty.net"]
11
+ spec.summary = %q{fat_core provides some useful core extensions}
12
+ spec.description = %q{Write a longer description. Optional.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency "rcodetools"
25
+ spec.add_development_dependency "byebug"
26
+
27
+ spec.add_runtime_dependency "activesupport"
28
+ spec.add_runtime_dependency "erubis"
29
+ end
data/lib/fat_core.rb ADDED
@@ -0,0 +1,19 @@
1
+
2
+ require 'date'
3
+ require 'active_support'
4
+ require 'active_support/core_ext'
5
+
6
+ require "fat_core/version"
7
+
8
+ require 'fat_core/array'
9
+ require 'fat_core/date'
10
+ require 'fat_core/enumerable'
11
+ require 'fat_core/hash'
12
+ require 'fat_core/kernel'
13
+ require 'fat_core/latex_eruby'
14
+ require 'fat_core/nil'
15
+ require 'fat_core/numeric'
16
+ require 'fat_core/period'
17
+ require 'fat_core/range'
18
+ require 'fat_core/string'
19
+ require 'fat_core/symbol'
@@ -0,0 +1,14 @@
1
+ class Array
2
+ # Duplicate each element of an Array into a new Array
3
+ def dup2
4
+ newa = []
5
+ self.each { |e|
6
+ newa << e.dup
7
+ }
8
+ return newa
9
+ end
10
+
11
+ def last_i
12
+ self.size - 1
13
+ end
14
+ end
@@ -0,0 +1,757 @@
1
+ require 'fat_core/period'
2
+
3
+ class Date
4
+ # Constants for Begining of Time (BOT) and End of Time (EOT)
5
+ # Both outside the range of what we would find in an accounting app.
6
+ BOT = Date.parse('1900-01-01')
7
+ EOT = Date.parse('3000-12-31')
8
+
9
+ # Convert a string with an American style date into a Date object
10
+ #
11
+ # An American style date is of the form MM/DD/YYYY, that is it places the
12
+ # month first, then the day of the month, and finally the year. The
13
+ # European convention is to place the day of the month first, DD/MM/YYYY.
14
+ # Because a date found in the wild can be ambiguous, e.g. 3/5/2014, a date
15
+ # string known to be using the American convention can be parsed using this
16
+ # method. Both the month and the day can be a single digit. The year can
17
+ # be either 2 or 4 digits, and if given as 2 digits, it adds 2000 to it to
18
+ # give the year.
19
+ #
20
+ # @example
21
+ # Date.parse_american('9/11/2001') #=> Date(2011, 9, 11)
22
+ # Date.parse_american('9/11/01') #=> Date(2011, 9, 11)
23
+ # Date.parse_american('9/11/1') #=> ArgumentError
24
+ #
25
+ # @param str [#to_s] a stringling of the form MM/DD/YYYY
26
+ # @return [Date] the date represented by the string paramenter.
27
+ def self.parse_american(str)
28
+ if str.to_s =~ %r{\A\s*(\d\d?)\s*/\s*(\d\d?)\s*/\s*(\d?\d?\d\d)\s*\z}
29
+ year, month, day = $3.to_i, $1.to_i, $2.to_i
30
+ if year < 100
31
+ year += 2000
32
+ end
33
+ Date.new(year, month, day)
34
+ else
35
+ raise ArgumentError, "date string must be of form 'MM?/DD?/YY(YY)?'"
36
+ end
37
+ end
38
+
39
+ =begin
40
+ Convert a 'date spec' to a Date. A date spec is a short-hand way of
41
+ specifying a date, relative to the computer clock. A date spec can
42
+ interpreted as either a 'from spec' or a 'to spec'.
43
+
44
+ @example
45
+ Assuming that Date.current at the time of execution is 2014-07-26 and
46
+ using the default spec_type of :from. The return values are actually Date
47
+ objects, but are shown below as textual dates.
48
+
49
+ A fully specified date returns that date:
50
+ Date.parse_spec('2001-09-11') # =>
51
+
52
+ Commercial weeks can be specified using, for example W32 or 32W, with the
53
+ week beginning on Monday, ending on Sunday.
54
+ Date.parse_spec('2012-W32') # =>
55
+ Date.parse_spec('2012-W32', :to) # =>
56
+ Date.parse_spec('W32') # =>
57
+
58
+ A spec of the form Q3 or 3Q returns the beginning or end of calendar
59
+ quarters.
60
+ Date.parse_spec('Q3') # =>
61
+
62
+ @param spec [#to_s] a stringling containing the spec to be interpreted
63
+ @param spec_type [:from, :to] interpret the spec as a from- or to-spec
64
+ respectively, defaulting to interpretation as a to-spec.
65
+ @return [Date] a date object equivalent to the date spec
66
+ =end
67
+ def self.parse_spec(spec, spec_type = :from)
68
+ spec = spec.to_s.strip
69
+ unless [:from, :to].include?(spec_type)
70
+ raise ArgumentError "invalid date spec type: '#{spec_type}'"
71
+ end
72
+
73
+ today = Date.current
74
+ case spec
75
+ when /^(\d\d\d\d)-(\d\d?)-(\d\d?)*$/
76
+ # A specified date
77
+ Date.new($1.to_i, $2.to_i, $3.to_i)
78
+ when /\AW(\d\d?)\z/, /\A(\d\d?)W\z/
79
+ week_num = $1.to_i
80
+ if week_num < 1 || week_num > 53
81
+ raise ArgumentError, "invalid week number (1-53): 'W#{week_num}'"
82
+ end
83
+ spec_type == :from ? Date.commercial(today.year, week_num).beginning_of_week :
84
+ Date.commercial(today.year, week_num).end_of_week
85
+ when /\A(\d\d\d\d)-W(\d\d?)\z/, /\A(\d\d\d\d)-(\d\d?)W\z/
86
+ year = $1.to_i
87
+ week_num = $2.to_i
88
+ if week_num < 1 || week_num > 53
89
+ raise ArgumentError, "invalid week number (1-53): 'W#{week_num}'"
90
+ end
91
+ spec_type == :from ? Date.commercial(year, week_num).beginning_of_week :
92
+ Date.commercial(year, week_num).end_of_week
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
+ spec_type == :from ? Date.new(year, month, 1).beginning_of_quarter :
102
+ Date.new(year, month, 1).end_of_quarter
103
+ when /^([1234])[qQ]$/, /^[qQ]([1234])$/
104
+ # Quarter only
105
+ this_year = today.year
106
+ quarter = $1.to_i
107
+ date = Date.new(this_year, quarter * 3, 15)
108
+ spec_type == :from ? date.beginning_of_quarter : date.end_of_quarter
109
+ when /^(\d\d\d\d)-(\d\d?)*$/
110
+ # Year-Month only
111
+ spec_type == :from ? Date.new($1.to_i, $2.to_i, 1) :
112
+ Date.new($1.to_i, $2.to_i, 1).end_of_month
113
+ when /\A(\d\d?)\z/
114
+ # Month only
115
+ spec_type == :from ? Date.new(today.year, $1.to_i, 1) :
116
+ Date.new(today.year, $1.to_i, 1).end_of_month
117
+ when /^(\d\d\d\d)$/
118
+ # Year only
119
+ spec_type == :from ? Date.new($1.to_i, 1, 1) : Date.new($1.to_i, 12, 31)
120
+ when /^(to|this_?)?day/
121
+ today
122
+ when /^(yester|last_?)?day/
123
+ today - 1.day
124
+ when /^(this_?)?week/
125
+ spec_type == :from ? today.beginning_of_week : today.end_of_week
126
+ when /last_?week/
127
+ spec_type == :from ? (today - 1.week).beginning_of_week :
128
+ (today - 1.week).end_of_week
129
+ when /^(this_?)?biweek/
130
+ spec_type == :from ? today.beginning_of_biweek : today.end_of_biweek
131
+ when /last_?biweek/
132
+ spec_type == :from ? (today - 1.week).beginning_of_biweek :
133
+ (today - 1.week).end_of_biweek
134
+ when /^(this_?)?semimonth/
135
+ spec_type == :from ? today.beginning_of_semimonth : today.end_of_semimonth
136
+ when /^last_?semimonth/
137
+ spec_type == :from ? (today - 15.days).beginning_of_semimonth :
138
+ (today - 15.days).end_of_semimonth
139
+ when /^(this_?)?month/
140
+ spec_type == :from ? today.beginning_of_month : today.end_of_month
141
+ when /^last_?month/
142
+ spec_type == :from ? (today - 1.month).beginning_of_month :
143
+ (today - 1.month).end_of_month
144
+ when /^(this_?)?bimonth/
145
+ spec_type == :from ? today.beginning_of_bimonth : today.end_of_bimonth
146
+ when /^last_?bimonth/
147
+ spec_type == :from ? (today - 1.month).beginning_of_bimonth :
148
+ (today - 1.month).end_of_bimonth
149
+ when /^(this_?)?quarter/
150
+ spec_type == :from ? today.beginning_of_quarter : today.end_of_quarter
151
+ when /^last_?quarter/
152
+ spec_type == :from ? (today - 3.months).beginning_of_quarter :
153
+ (today - 3.months).end_of_quarter
154
+ when /^(this_?)?year/
155
+ spec_type == :from ? today.beginning_of_year : today.end_of_year
156
+ when /^last_?year/
157
+ spec_type == :from ? (today - 1.year).beginning_of_year :
158
+ (today - 1.year).end_of_year
159
+ when /^forever/
160
+ spec_type == :from ? Date::BOT : Date::EOT
161
+ when /^never/
162
+ nil
163
+ else
164
+ raise ArgumentError, "bad date spec: '#{spec}''"
165
+ end # !> previous definition of length was here
166
+ end
167
+
168
+ def weekend?
169
+ saturday? || sunday?
170
+ end
171
+
172
+ def weekday?
173
+ !weekend?
174
+ end
175
+
176
+ def pred
177
+ self - 1.day
178
+ end
179
+
180
+ def succ
181
+ self + 1.day
182
+ end
183
+
184
+ def iso
185
+ strftime("%Y-%m-%d")
186
+ end
187
+
188
+ def org
189
+ strftime("[%Y-%m-%d %a]")
190
+ end
191
+
192
+ def eng
193
+ strftime("%B %e, %Y")
194
+ end
195
+
196
+ def quarter
197
+ case month
198
+ when (1..3)
199
+ 1
200
+ when (4..6)
201
+ 2
202
+ when (7..9)
203
+ 3
204
+ when (10..12)
205
+ 4
206
+ end
207
+ end
208
+
209
+ def beginning_of_bimonth
210
+ if month % 2 == 1
211
+ beginning_of_month
212
+ else
213
+ (self - 1.month).beginning_of_month
214
+ end
215
+ end
216
+
217
+ def end_of_bimonth
218
+ if month % 2 == 1
219
+ (self + 1.month).end_of_month
220
+ else
221
+ end_of_month
222
+ end
223
+ end
224
+
225
+ def beginning_of_semimonth
226
+ if day >= 16
227
+ Date.new(year, month, 16)
228
+ else
229
+ beginning_of_month
230
+ end
231
+ end
232
+
233
+ def end_of_semimonth
234
+ if day <= 15
235
+ Date.new(year, month, 15)
236
+ else
237
+ end_of_month
238
+ end
239
+ end
240
+
241
+ # Note: we use a monday start of the week in the next two methods because
242
+ # commercial week counting assumes a monday start.
243
+ def beginning_of_biweek
244
+ if cweek % 2 == 1
245
+ beginning_of_week(:monday)
246
+ else
247
+ (self - 1.week).beginning_of_week(:monday)
248
+ end
249
+ end
250
+
251
+ def end_of_biweek
252
+ if cweek % 2 == 1
253
+ (self + 1.week).end_of_week(:monday)
254
+ else
255
+ end_of_week(:monday)
256
+ end
257
+ end
258
+
259
+ def beginning_of_year?
260
+ self.beginning_of_year == self
261
+ end
262
+
263
+ def end_of_year?
264
+ self.end_of_year == self
265
+ end
266
+
267
+ def beginning_of_quarter?
268
+ self.beginning_of_quarter == self
269
+ end
270
+
271
+ def end_of_quarter?
272
+ self.end_of_quarter == self
273
+ end
274
+
275
+ def beginning_of_bimonth?
276
+ month % 2 == 1 &&
277
+ self.beginning_of_month == self
278
+ end
279
+
280
+ def end_of_bimonth?
281
+ month % 2 == 0 &&
282
+ self.end_of_month == self
283
+ end
284
+
285
+ def beginning_of_month?
286
+ self.beginning_of_month == self
287
+ end
288
+
289
+ def end_of_month?
290
+ self.end_of_month == self
291
+ end
292
+
293
+ def beginning_of_semimonth?
294
+ self.beginning_of_semimonth == self
295
+ end
296
+
297
+ def end_of_semimonth?
298
+ self.end_of_semimonth == self
299
+ end
300
+
301
+ def beginning_of_biweek?
302
+ self.beginning_of_biweek == self
303
+ end
304
+
305
+ def end_of_biweek?
306
+ self.end_of_biweek == self
307
+ end
308
+
309
+ def beginning_of_week?
310
+ self.beginning_of_week == self
311
+ end
312
+
313
+ def end_of_week?
314
+ self.end_of_week == self
315
+ end
316
+
317
+ # Format date in MM/DD/YYYY form
318
+ def american
319
+ strftime "%-m/%-d/%Y"
320
+ end
321
+
322
+ def expand_to_period(sym)
323
+ Period.new(beginning_of_chunk(sym), end_of_chunk(sym))
324
+ end
325
+
326
+ def add_chunk(chunk)
327
+ case chunk
328
+ when :year
329
+ next_year
330
+ when :quarter
331
+ next_month(3)
332
+ when :bimonth
333
+ next_month(2)
334
+ when :month
335
+ next_month
336
+ when :semimonth
337
+ self + 15.days
338
+ when :biweek
339
+ self + 14.days
340
+ when :week
341
+ self + 7.days
342
+ when :day
343
+ self + 1.days
344
+ else
345
+ raise LogicError, "add_chunk unknown chunk: '#{chunk}'"
346
+ end
347
+ end
348
+
349
+ def beginning_of_chunk(sym)
350
+ case sym
351
+ when :year
352
+ beginning_of_year
353
+ when :quarter
354
+ beginning_of_quarter
355
+ when :bimonth
356
+ beginning_of_bimonth
357
+ when :month
358
+ beginning_of_month
359
+ when :semimonth
360
+ beginning_of_semimonth
361
+ when :biweek
362
+ beginning_of_biweek
363
+ when :week
364
+ beginning_of_week
365
+ when :day
366
+ self
367
+ else
368
+ raise LogicError, "unknown chunk sym: '#{sym}'"
369
+ end
370
+ end
371
+
372
+ def end_of_chunk(sym)
373
+ case sym
374
+ when :year
375
+ end_of_year
376
+ when :quarter
377
+ end_of_quarter
378
+ when :bimonth
379
+ end_of_bimonth
380
+ when :month
381
+ end_of_month
382
+ when :semimonth
383
+ end_of_semimonth
384
+ when :biweek
385
+ end_of_biweek
386
+ when :week
387
+ end_of_week
388
+ when :day
389
+ self
390
+ else
391
+ raise LogicError, "unknown chunk sym: '#{sym}'"
392
+ end
393
+ end
394
+
395
+ # Holidays decreed by executive order
396
+ FED_DECLARED_HOLIDAYS =
397
+ [
398
+ Date.parse('2012-12-24')
399
+ ]
400
+
401
+ def self.days_in_month(y, m)
402
+ days = Time::COMMON_YEAR_DAYS_IN_MONTH[m]
403
+ return(days) unless m == 2
404
+ return Date.new(y, m, 1).leap? ? 29 : 28
405
+ end
406
+
407
+ def self.nth_wday_in_year_month(n, wday, year, month)
408
+ # Return the nth weekday in the given month
409
+ # If n is negative, count from last day of month
410
+ if n > 0
411
+ # Set d to the 1st wday in month
412
+ d = Date.new(year, month, 1)
413
+ while d.wday != wday
414
+ d += 1
415
+ end
416
+ # Set d to the nth wday in month
417
+ nd = 1
418
+ while nd != n
419
+ d += 7
420
+ nd += 1
421
+ end
422
+ return d
423
+ elsif n < 0
424
+ n = -n
425
+ # Set d to the last wday in month
426
+ d = Date.new(year, month,
427
+ Date.last_day_in_year_month(year, month))
428
+ while d.wday != wday;
429
+ d -= 1
430
+ end
431
+ # Set d to the nth wday in month
432
+ nd = 1
433
+ while nd != n
434
+ d -= 7
435
+ nd += 1
436
+ end
437
+ return d
438
+ else
439
+ raise ArgumentError,
440
+ 'Arg 1 to nth_wday_in_month_year cannot be zero'
441
+ end
442
+ end
443
+
444
+ def self.last_day_in_year_month(year, month)
445
+ days = [
446
+ 31, # Dec
447
+ 31, # Jan
448
+ 28, # Feb
449
+ 31, # Mar
450
+ 30, # Apr
451
+ 31, # May
452
+ 30, # Jun
453
+ 31, # Jul
454
+ 31, # Aug
455
+ 30, # Sep
456
+ 31, # Oct
457
+ 30, # Nov
458
+ 31, # Dec
459
+ ]
460
+ days[2] = 29 if Date.new(year, month, 1).leap?
461
+ days[month % 12]
462
+ end
463
+
464
+ def easter_this_year
465
+ # Return the date of Easter in self's year
466
+ y = self.year
467
+ a = y % 19
468
+ b, c = y.divmod(100)
469
+ d, e = b.divmod(4)
470
+ f = (b + 8) / 25
471
+ g = (b - f + 1) / 3
472
+ h = (19 * a + b - d - g + 15) % 30
473
+ i, k = c.divmod(4)
474
+ l = (32 + 2*e + 2*i - h - k) % 7
475
+ m = (a + 11*h + 22*l) / 451
476
+ n, p = (h + l - 7*m + 114).divmod(31)
477
+ return Date.new(y, n, p + 1)
478
+ end
479
+
480
+ def easter?
481
+ # Am I Easter?
482
+ # Easter is always in March or April
483
+ return false unless [3, 4].include?(self.mon)
484
+ return self == self.easter_this_year
485
+ end
486
+
487
+ def nth_wday_in_month?(n, wday, month)
488
+ # Is self the nth weekday in the given month of its year?
489
+ # If n is negative, count from last day of month
490
+ if self.wday != wday
491
+ return false
492
+ elsif self.mon != month
493
+ return false
494
+ else
495
+ return self == Date.nth_wday_in_year_month(n, wday, self.year, month)
496
+ end
497
+ end
498
+
499
+ def fed_fixed_holiday?
500
+ # Fixed-date holidays on weekdays
501
+ if self.mon == 1 && self.mday == 1
502
+ # New Years (January 1),
503
+ return true
504
+ elsif self.mon == 7 && self.mday == 4
505
+ # Independence Day (July 4),
506
+ return true
507
+ elsif self.mon == 11 && self.mday == 11
508
+ # Veterans Day (November 11),
509
+ return true
510
+ elsif self.mon == 12 && self.mday == 25
511
+ # Christmas (December 25), and
512
+ return true
513
+ elsif self.mon == 12 && self.mday == 31
514
+ # New Year's Eve (December 31)
515
+ return true;
516
+ else
517
+ return false
518
+ end
519
+ end
520
+
521
+ def fed_moveable_feast?
522
+ # See if today is a "movable feast," all of which are
523
+ # rigged to fall on Monday except Thanksgiving
524
+
525
+ # No moveable feasts in certain months
526
+ if [ 3, 4, 6, 7, 8, 12 ].include?(self.month)
527
+ return false
528
+ elsif self.wday == 1
529
+ # MLK's Birthday (Third Monday in Jan)
530
+ return true if self.nth_wday_in_month?(3, 1, 1)
531
+ # Washington's Birthday (Third Monday in Feb)
532
+ return true if self.nth_wday_in_month?(3, 1, 2)
533
+ # Memorial Day (Last Monday in May)
534
+ return true if self.nth_wday_in_month?(-1, 1, 5)
535
+ # Labor Day (First Monday in Sep)
536
+ return true if self.nth_wday_in_month?(1, 1, 9)
537
+ # Columbus Day (Second Monday in Oct)
538
+ return true if self.nth_wday_in_month?(2, 1, 10)
539
+ # Other Mondays
540
+ return false
541
+ elsif self.wday == 4
542
+ # Thanksgiving Day (Fourth Thur in Nov)
543
+ return false unless self.month == 11
544
+ return self.nth_wday_in_month?(4, 4, 11)
545
+ else
546
+ return false
547
+ end
548
+ end
549
+
550
+ def fed_holiday?
551
+ if FED_DECLARED_HOLIDAYS.include?(self)
552
+ return true
553
+ end
554
+
555
+ # All Saturdays and Sundays are "holidays"
556
+ if self.weekend? then return true end
557
+
558
+ # Is self a fixed holiday
559
+ return true if self.fed_fixed_holiday?
560
+
561
+ # A Friday is a holiday if a fixed-date holiday
562
+ # would fall on the following Saturday
563
+ if self.wday == 5
564
+ td = self + 1
565
+ return true if td.fed_fixed_holiday?
566
+ end
567
+
568
+ # A Monday is a holiday if a fixed-date holiday
569
+ # would fall on the preceding Sunday
570
+ if self.wday == 1
571
+ td = self - 1
572
+ return true if td.fed_fixed_holiday?
573
+ end
574
+
575
+ # If Christmas falls on a Thursday, apparently, the Friday after is
576
+ # treated as a holiday as well. See 2003, 2008, for example.
577
+ if self.wday == 5 and self.month == 12 and self.day == 26
578
+ return true
579
+ end
580
+
581
+ # It's last chance is if its a movable feast
582
+ return self.fed_moveable_feast?;
583
+ end
584
+
585
+ #######################################################
586
+ # Calulations for NYSE holidays
587
+ # Rule 51 and supplementary material
588
+ #######################################################
589
+
590
+ # Rule: if it falls on Saturday, observe on preceding Friday.
591
+ # Rule: if it falls on Sunday, observe on following Monday.
592
+ #
593
+ # New Year's Day, January 1.
594
+ # Birthday of Martin Luther King, Jr., the third Monday in January.
595
+ # Washington's Birthday, the third Monday in February.
596
+ # Good Friday Friday before Easter Sunday. NOTE: not a fed holiday
597
+ # Memorial Day, the last Monday in May.
598
+ # Independence Day, July 4.
599
+ # Labor Day, the first Monday in September.
600
+ # NOTE: Columbus and Veterans days not observed
601
+ # Thanksgiving Day, the fourth Thursday in November.
602
+ # Christmas Day, December 25.
603
+
604
+ def nyse_fixed_holiday?
605
+ # Fixed-date holidays
606
+ if self.mon == 1 && self.mday == 1
607
+ # New Years (January 1),
608
+ return true
609
+ elsif self.mon == 7 && self.mday == 4
610
+ # Independence Day (July 4),
611
+ return true
612
+ elsif self.mon == 12 && self.mday == 25
613
+ # Christmas (December 25), and
614
+ return true
615
+ else
616
+ return false
617
+ end
618
+ end
619
+
620
+ def nyse_moveable_feast?
621
+ # See if today is a "movable feast," all of which are
622
+ # rigged to fall on Monday except Thanksgiving
623
+
624
+ # No moveable feasts in certain months
625
+ if [ 6, 7, 8, 10, 12 ].include?(self.month)
626
+ return false
627
+ elsif self.wday == 1
628
+ # MLK's Birthday (Third Monday in Jan)
629
+ return true if self.nth_wday_in_month?(3, 1, 1)
630
+ # Washington's Birthday (Third Monday in Feb)
631
+ return true if self.nth_wday_in_month?(3, 1, 2)
632
+ # Memorial Day (Last Monday in May)
633
+ return true if self.nth_wday_in_month?(-1, 1, 5)
634
+ # Labor Day (First Monday in Sep)
635
+ return true if self.nth_wday_in_month?(1, 1, 9)
636
+ # Other Mondays
637
+ return false
638
+ elsif self.wday == 4
639
+ # Thanksgiving Day (Fourth Thur in Nov)
640
+ return false unless self.month == 11
641
+ return self.nth_wday_in_month?(4, 4, 11)
642
+ elsif self.wday == 5
643
+ # Good Friday, the Friday before Easter
644
+ td = self + 2
645
+ return td.easter?
646
+ else
647
+ return false
648
+ end
649
+ end
650
+
651
+ def nyse_holiday?
652
+ # All Saturdays and Sundays are "holidays"
653
+ return true if self.weekend?
654
+
655
+ # Is self a fixed holiday
656
+ return true if self.nyse_fixed_holiday?
657
+
658
+ # A Friday is a holiday if a fixed-date holiday
659
+ # would fall on the following Saturday
660
+ if self.wday == 5
661
+ td = self + 1
662
+ return true if td.nyse_fixed_holiday?
663
+ end
664
+
665
+ # A Monday is a holiday if a fixed-date holiday
666
+ # would fall on the preceding Sunday
667
+ if self.wday == 1
668
+ td = self - 1
669
+ return true if td.nyse_fixed_holiday?
670
+ end
671
+
672
+ # It's last chance is if its a movable feast
673
+ return self.nyse_moveable_feast?;
674
+ end
675
+
676
+ def fed_workday?
677
+ return ! self.fed_holiday?;
678
+ end
679
+
680
+ def nyse_workday?
681
+ return ! self.nyse_holiday?;
682
+ end
683
+
684
+ def next_fed_workday
685
+ result = self + 1
686
+ while result.fed_holiday?
687
+ result += 1;
688
+ end
689
+ return result
690
+ end
691
+
692
+ def add_fed_business_days(n)
693
+ d = self.dup
694
+ if n < 0
695
+ n.abs.times { d = d.prior_fed_workday }
696
+ elsif n > 0
697
+ n.times { d = d.next_fed_workday }
698
+ end
699
+ d
700
+ end
701
+
702
+ def next_nyse_workday
703
+ result = self.dup
704
+ result += 1
705
+ while result.nyse_holiday?
706
+ result += 1;
707
+ end
708
+ return result
709
+ end
710
+
711
+ def add_nyse_business_days(n)
712
+ d = self.dup
713
+ if n < 0
714
+ n.abs.times { d = d.prior_nyse_workday }
715
+ elsif n > 0
716
+ n.times { d = d.next_nyse_workday }
717
+ end
718
+ d
719
+ end
720
+
721
+ def prior_fed_workday
722
+ result = self - 1
723
+ while result.fed_holiday?
724
+ result -= 1;
725
+ end
726
+ return result
727
+ end
728
+
729
+ def prior_nyse_workday
730
+ result = self.dup
731
+ result -= 1
732
+ while result.nyse_holiday?
733
+ result -= 1;
734
+ end
735
+ return result
736
+ end
737
+
738
+ def iso
739
+ strftime("%Y-%m-%d")
740
+ end
741
+
742
+ def num
743
+ strftime("%Y%m%d")
744
+ end
745
+
746
+ def within_6mos_of?(d)
747
+ # Date 6 calendar months before self
748
+ start_date = self - 6.months + 2.days
749
+ end_date = self + 6.months - 2.days
750
+ (start_date..end_date).cover?(d)
751
+ end
752
+
753
+ # Allow erb documents can directly interpolate dates
754
+ def tex_quote
755
+ iso
756
+ end
757
+ end