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 +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.yardopts +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +46 -0
- data/Rakefile +1 -0
- data/fat_core.gemspec +29 -0
- data/lib/fat_core.rb +19 -0
- data/lib/fat_core/array.rb +14 -0
- data/lib/fat_core/date.rb +757 -0
- data/lib/fat_core/enumerable.rb +7 -0
- data/lib/fat_core/hash.rb +33 -0
- data/lib/fat_core/kernel.rb +9 -0
- data/lib/fat_core/latex_eruby.rb +11 -0
- data/lib/fat_core/nil.rb +9 -0
- data/lib/fat_core/numeric.rb +87 -0
- data/lib/fat_core/period.rb +410 -0
- data/lib/fat_core/range.rb +192 -0
- data/lib/fat_core/string.rb +184 -0
- data/lib/fat_core/symbol.rb +17 -0
- data/lib/fat_core/version.rb +3 -0
- data/spec/lib/date_spec.rb +320 -0
- data/spec/lib/kernel_spec.rb +11 -0
- data/spec/lib/numeric_spec.rb +34 -0
- data/spec/lib/period_spec.rb +294 -0
- data/spec/lib/range_spec.rb +246 -0
- data/spec/lib/string_spec.rb +128 -0
- data/spec/spec_helper.rb +23 -0
- metadata +178 -0
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
data/.rspec
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private --protected lib/**/*.rb - README LICENSE
|
data/Gemfile
ADDED
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,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
|