fat_core 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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