fat_core 3.0.0 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1683ad80cc0beeb11788dc2b0309f61879109de5
4
- data.tar.gz: 35f7be2a9a512700ebcc327bff6df8a59ae9b4cf
3
+ metadata.gz: 1a7476217f8c6438e15010dcb43a6b3e5a7fbdc3
4
+ data.tar.gz: 99d35b1291dfa588bef9fbed7185e82c12e6b8ad
5
5
  SHA512:
6
- metadata.gz: 8f0d3d2b01f59d9bd931e477226d3a7631005bdf2c8bc6912c0baaeaa8dc3bf0f97be32bd3851992d75e37b0790b57e406007f7ff7fa8f96c233ce8f3cc4bbff
7
- data.tar.gz: b243a502b75d8501e86b899b00cebdfb09ae35659615b0611e82284fdd2497d55df952e381208b32efadbd59c082b8962f6750ede8989a245d0bd81ee5efd87f
6
+ metadata.gz: 003b3ab76e3d11011b7f67504ea0422d1e34cb8c4f32606a4f0b28e216416d13f18d5e713c78ca30200ded965076e2593b75ff80b0002ff35fffc5abdf487cb9
7
+ data.tar.gz: 901ad06b503df0199ef15ccf46232d2552cd8ae0eb76bbb0b29da4e4e7036a82b274d5be3b07587917116d2450ae608a877714a3dcce0ca4d03b8658083f1ba6
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 2.3.1
1
+ 2.3.3
data/.yardopts CHANGED
@@ -1 +1,5 @@
1
- --no-private --protected lib/**/*.rb - README LICENSE
1
+ --no-private
2
+ --embed-mixins
3
+ --readme README.md
4
+ --markup-provider=redcarpet
5
+ --markup=markdown
data/README.md CHANGED
@@ -4,34 +4,151 @@ fat-core is a simple gem to collect core extensions and a few new classes that
4
4
  I find useful in multiple projects. The emphasis is on extending the Date
5
5
  class to make it more useful in financial applications.
6
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.
7
+ ## Usage
8
+
9
+ You can extend classes individually by requiring the corresponding file:
10
+
11
+ ```
12
+ require 'fat_core/array'
13
+ require 'fat_core/bigdecimal'
14
+ require 'fat_core/date'
15
+ require 'fat_core/enumerable'
16
+ require 'fat_core/hash'
17
+ require 'fat_core/kernel'
18
+ require 'fat_core/numeric'
19
+ require 'fat_core/range'
20
+ require 'fat_core/string'
21
+ require 'fat_core/symbol'
22
+ ```
23
+
24
+ Or, you can require them all:
25
+
26
+ ```
27
+ require 'fat_core/all'
28
+ ```
9
29
 
30
+ Many of these have little that is of general interest, but there are a few
31
+ goodies.
32
+
33
+ ### Date
34
+
35
+ For example, the `Date` class adds two methods for determining whether a given
36
+ date is a US federal holiday as defined by federal law, including such things as
37
+ federal holidays established by executive decree:
38
+
39
+ ```
40
+ require 'fat_core/date'
10
41
  Date.parse('2014-05-18').fed_holiday? => true # It's a weekend
11
42
  Date.parse('2014-01-01').fed_holiday? => true # It's New Years
12
-
13
- All holidays defined by federal statute are recognized.
43
+ ```
14
44
 
15
45
  Likewise, days on which the NYSE is closed can be gotten with:
16
46
 
47
+ ```
17
48
  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.
49
+ ```
50
+
51
+ Conversely, `Date#fed_workday?` and `Date#nyse_workday?` return true if the
52
+ federal government and the NYSE respectively are open for business on those
53
+ days.
54
+
55
+ In addition, the Date class, as extended by FatCore, adds `#next_<chunk>`
56
+ methods for calendar periods in addition to those provided by the core Date
57
+ class: `#next_half`, `#next_quarter`, `#next_bimonth`, and `#next_semimonth`,
58
+ `#next_biweek`. There are also `#prior_<chunk>` variants of these, as well as
59
+ methods for finding the end and beginning of all these periods (e.g.,
60
+ `#beginning_of_bimonth`) and for querying whether a Date is at the beginning or
61
+ end of these periods (e.g., `#beginning_of_bimonth?`, `#end_of_bimonth?`, etc.).
62
+
63
+ FatCore also provides convenience formatting methods, such as `Date#iso` for
64
+ quickly converting a Date to a string of the form 'YYYY-MM-DD', `Date#org` for
65
+ formatting a Date as an Emacs org-mode timestamp, and several others.
66
+
67
+ Finally, it provides a `#parse_spec` method for parsing a string, typically
68
+ provided by a user, allowing all the period chunks to be conveniently and
69
+ tersely specified by a user. For example, the string '2Q' will be parsed as the
70
+ second calendar quarter of the current year, while '2014-3Q' will be parsed as
71
+ the third quarter of the year 2014.
72
+
73
+ ### Range
74
+
75
+ You can also extend the Range class with several useful methods that emphasize
76
+ coverage of one range by one or more others (`#spanned_by?` and `#gaps`),
77
+ contiguity of Ranges to one another (`#contiguous?`, `#left_contiguous`, and
78
+ `#right_contiguous`, `#join`), and the testing of overlaps between ranges
79
+ (`#overlaps?`, `#overlaps_among?`). These are put to good use in the
80
+ 'fat_period' (https://github.com/ddoherty03/fat_period) gem, which combines
81
+ fat_core's extended Range class with its extended Date class to make a useful
82
+ Period class for date ranges, and you may find fat_core's extended Range class
83
+ likewise useful.
84
+
85
+ For example, you can use the `#gaps` method to find the gaps left in the
86
+ coverage on one Range by an Array of other Ranges:
87
+
88
+ ```
89
+ require 'fat_core/range'
90
+ (0..12).gaps([(0..2), (5..7), (10..12)]) => [(3..4), (8..9)]
91
+ ```
92
+
93
+ ### Enumerable
94
+
95
+ FatCore::Enumerable extends Enumerable with the `#each_with_flags` method that
96
+ yields the elements of the Enumerable but also yields two booleans, `first` and
97
+ `last` that are set to true on respectively, the first and last element of the
98
+ Enumerable. This makes it easy to treat these two cases specially without
99
+ testing the index as in `#each_with_index`.
100
+
101
+ ### Hash
102
+
103
+ FatCore::Hash extends the Hash class with some useful methods for element
104
+ deletion (`#delete_with_value`) and for manipulating the keys
105
+ (`#keys_with_value`, `#remap_keys` and `#replace_keys`) of a Hash. It also
106
+ provides `#each_pair_with_flags` as an analog to Enumerable's
107
+ `#each_with_flags`.
108
+
109
+ ### TeX Quoting
110
+
111
+ Several of the extension, most notably 'fat_core/string', provides a
112
+ `#tex_quote` method for quoting the string version of an object so as to allow
113
+ its inclusion in a TeX document and quote characters such as '$' or '%' that
114
+ have a special meaning for TeX.
115
+
116
+ ### String
117
+
118
+ FatCore::String has methods for performing matching of one string with another
119
+ (`#matches_with`, `#fuzzy_match`), for converting a string to title-case as
120
+ might by used in the title of a book (`#entitle`), for converting a String into
121
+ a useable Symbol (`#as_sym`) and vice-versa (`#as_string` also
122
+ `Symbol#as_string`), for wrapping with an optional hanging indent (`#wrap`),
123
+ cleaning up errant spaces (`#clean`), and computing the Damerau-Levenshtein
124
+ distance between strings (`#distance`). And several others.
125
+
126
+ ### Numbers
127
+
128
+ FatCore::Numeric has methods for inserting grouping commas into a number
129
+ (`#commas` and `#group`), for converting seconds to HH:MM:SS.dd format
130
+ (`#secs_to_hms`), for testing for integrality (`#whole?` and `#int_if_whole`), and
131
+ testing for sign (`#signum`).
21
132
 
22
133
  ## Installation
23
134
 
24
135
  Add this line to your application's Gemfile:
25
136
 
137
+ ```
26
138
  gem 'fat_core', :git => 'https://github.com/ddoherty03/fat_core.git'
139
+ ```
27
140
 
28
141
  And then execute:
29
142
 
143
+ ```
30
144
  $ bundle
145
+ ```
31
146
 
32
147
  Or install it yourself as:
33
148
 
149
+ ```
34
150
  $ gem install fat_core
151
+ ```
35
152
 
36
153
  ## Usage
37
154
 
data/Rakefile CHANGED
@@ -1,6 +1,22 @@
1
1
  require 'bundler/gem_tasks'
2
-
3
2
  require 'rspec/core/rake_task'
3
+ require 'rdoc/task'
4
+ require 'yard'
5
+
6
+ RDoc::Task.new do |rdoc|
7
+ rdoc.main = 'README.rdoc'
8
+ rdoc.rdoc_files.include('README.rdoc', 'lib/')
9
+ rdoc.options << "--ri"
10
+ end
11
+
12
+ YARD::Rake::YardocTask.new do |t|
13
+ t.files = ['lib/**/*.rb', 'README.md']
14
+ t.options << '--no-private'
15
+ t.options << '--embed-mixins'
16
+ t.options << '--markup=markdown'
17
+ t.options << '--markup-provider=redcarpet'
18
+ #t.stats_options = ['--list-undoc']
19
+ end
4
20
 
5
21
  RSpec::Core::RakeTask.new(:spec, :tag) do |t|
6
22
  t.rspec_opts = '--tag ~online -f p'
data/bin/console CHANGED
@@ -1,14 +1,13 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "fat_core"
3
+ require 'bundler/setup'
4
+ require 'fat_core/all'
5
+ require 'pry'
5
6
 
6
7
  # You can add fixtures and/or initialization code here to make experimenting
7
8
  # with your gem easier. You can also use a different console, if you like.
9
+ @dd1 = Date.parse('2016-01-31')
10
+ @dd2 = Date.parse('2016-01-30')
11
+ @dd3 = Date.parse('2016-01-29')
8
12
 
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- require "pry"
11
13
  Pry.start
12
-
13
- #require "irb"
14
- #IRB.start(__FILE__)
data/bin/easters CHANGED
@@ -1,6 +1,6 @@
1
1
  #! /usr/bin/env ruby
2
2
 
3
- require 'fat_core'
3
+ require 'fat_core/date'
4
4
 
5
5
  base = Date.new(30, 1, 1)
6
6
  3000.times do |k|
data/fat_core.gemspec CHANGED
@@ -13,10 +13,11 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = ''
14
14
  spec.license = 'MIT'
15
15
  spec.required_ruby_version = '>= 2.3.1'
16
+ spec.metadata['yard.run'] = 'yri' # use "yard" to build full HTML docs.
16
17
 
17
18
  spec.files = `git ls-files -z`.split("\x0")
18
19
  spec.files.reject! { |fn| fn =~ /^NYSE_closings.pdf/ }
19
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.executables = spec.files.grep(%r{^bin/easter}) { |f| File.basename(f) }
20
21
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
21
22
  spec.require_paths = ['lib']
22
23
 
@@ -28,7 +29,7 @@ Gem::Specification.new do |spec|
28
29
  spec.add_development_dependency 'pry'
29
30
  spec.add_development_dependency 'pry-doc'
30
31
  spec.add_development_dependency 'pry-byebug'
31
- spec.add_development_dependency 'rcodetools'
32
+ spec.add_development_dependency 'redcarpet'
32
33
 
33
34
  spec.add_runtime_dependency 'activesupport'
34
35
  spec.add_runtime_dependency 'erubis'
data/lib/fat_core/all.rb CHANGED
@@ -1,14 +1,11 @@
1
1
  require 'fat_core/array'
2
- require 'fat_core/big_decimal'
2
+ require 'fat_core/bigdecimal'
3
3
  require 'fat_core/date'
4
- require 'fat_core/boolean'
5
4
  require 'fat_core/enumerable'
6
5
  require 'fat_core/hash'
7
6
  require 'fat_core/kernel'
8
- #require 'fat_core/latex_eruby'
9
7
  require 'fat_core/nil'
10
8
  require 'fat_core/numeric'
11
- require 'fat_core/period'
12
9
  require 'fat_core/range'
13
10
  require 'fat_core/string'
14
11
  require 'fat_core/symbol'
@@ -32,4 +32,7 @@ module FatCore
32
32
  end
33
33
  end
34
34
 
35
- Array.include FatCore::Array
35
+ class Array
36
+ include FatCore::Array
37
+ # @!parse include FatCore::Array
38
+ end
@@ -0,0 +1,19 @@
1
+ require 'bigdecimal'
2
+
3
+ module FatCore
4
+ module BigDecimal
5
+ # Provide a human-readable display for BigDecimal. e.g., while debugging.
6
+ # The inspect method in BigDecimal is unreadable, as it exposes the
7
+ # underlying implementation, not the number's value. This corrects that.
8
+ #
9
+ # @return [String]
10
+ def inspect
11
+ to_f.to_s
12
+ end
13
+ end
14
+ end
15
+
16
+ class BigDecimal
17
+ prepend(FatCore::BigDecimal)
18
+ # @!parse include FatCore::BigDecimal
19
+ end
data/lib/fat_core/date.rb CHANGED
@@ -3,69 +3,129 @@ require 'active_support/core_ext/date'
3
3
  require 'active_support/core_ext/time'
4
4
  require 'active_support/core_ext/numeric/time'
5
5
  require 'active_support/core_ext/integer/time'
6
- require 'fat_core/string'
7
6
 
7
+ # ## FatCore Date Extensions
8
+ #
9
+ # The FatCore extensions to the Date class add the notion of several additional
10
+ # calendar periods besides years, months, and weeks to those provided for in the
11
+ # Date class and the active_support extensions to Date. In particular, there
12
+ # are several additional calendar subdivisions (called "chunks" in this
13
+ # documentation) supported by FatCore's extension to the Date class:
14
+ #
15
+ # * year,
16
+ # * half,
17
+ # * quarter,
18
+ # * bimonth,
19
+ # * month,
20
+ # * semimonth,
21
+ # * biweek,
22
+ # * week, and
23
+ # * day
24
+ #
25
+ # For each of those chunks, there are methods for finding the beginning and end
26
+ # of the chunk, for advancing or retreating a Date by the chunk, and for testing
27
+ # whether a Date is at the beginning or end of each of the chunk.
28
+ #
29
+ # FatCore's Date extension defines a few convenience formatting methods, such as
30
+ # Date#iso and Date#org for formatting Dates as ISO strings and as Emacs
31
+ # org-mode inactive timestamps respectively. It also has a few utility methods
32
+ # for determining the date of Easter, the number of days in any given month, and
33
+ # the Date of the nth workday in a given month (say the third Thursday in
34
+ # October, 2014).
35
+ #
36
+ # The Date extension defines a couple of class methods for parsing strings into
37
+ # Dates, especially Date.parse_spec, which allows Dates to be specified in a
38
+ # lazy way, either absolutely or relative to the computer's clock.
39
+ #
40
+ # Finally FatCore's Date extensions provide thorough methods for determining if
41
+ # a Date is a United States federal holiday or workday based on US law,
42
+ # including executive orders. It does the same for the New York Stock Exchange,
43
+ # based on the rules of the New York Stock Exchange, including dates on which
44
+ # the NYSE was closed for special reasons, such as the 9-11 attacks in 2001.
8
45
  module FatCore
9
46
  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')
47
+ # Constant for Beginning of Time (BOT) outside the range of what we would ever
48
+ # want to find in commercial situations.
49
+ BOT = ::Date.parse('1900-01-01')
14
50
 
15
- # Predecessor of self. Allows Date to work as a countable element
16
- # of a Range.
17
- def pred
18
- self - 1.day
19
- end
51
+ # Constant for End of Time (EOT) outside the range of what we would ever want
52
+ # to find in commercial situations.
53
+ EOT = ::Date.parse('3000-12-31')
20
54
 
21
- # Successor of self. Allows Date to work as a countable element
22
- # of a Range.
23
- def succ
24
- self + 1.day
25
- end
55
+ # :category: Formatting
56
+ # @group Formatting
26
57
 
27
- # Format as an ISO string.
58
+ # Format as an ISO string of the form `YYYY-MM-DD`.
59
+ # @return [String]
28
60
  def iso
29
61
  strftime('%Y-%m-%d')
30
62
  end
31
63
 
64
+ # :category: Formatting
65
+
32
66
  # Format date to TeX documents as ISO strings
67
+ # @return [String]
33
68
  def tex_quote
34
69
  iso
35
70
  end
36
71
 
37
- # Format as an all-numeric string, i.e. 'YYYYMMDD'
72
+ # :category: Formatting
73
+
74
+ # Format as an all-numeric string of the form `YYYYMMDD`
75
+ # @return [String]
38
76
  def num
39
77
  strftime('%Y%m%d')
40
78
  end
41
79
 
42
- # Format as an inactive Org date (see emacs org-mode)
80
+ # :category: Formatting
81
+
82
+ # Format as an inactive Org date timestamp of the form `[YYYY-MM-DD <dow>]`
83
+ # (see Emacs org-mode)
84
+ # @return [String]
43
85
  def org
44
86
  strftime('[%Y-%m-%d %a]')
45
87
  end
46
88
 
47
- # Format as an English string
89
+ # :category: Formatting
90
+
91
+ # Format as an English string, like `'January 12, 2016'`
92
+ # @return [String]
48
93
  def eng
49
94
  strftime('%B %e, %Y')
50
95
  end
51
96
 
52
- # Format date in MM/DD/YYYY form, as typical for the short American
97
+ # :category: Formatting
98
+
99
+ # Format date in `MM/DD/YYYY` form, as typical for the short American
53
100
  # form.
101
+ # @return [String]
54
102
  def american
55
103
  strftime '%-m/%-d/%Y'
56
104
  end
57
105
 
106
+ # :category: Queries
107
+ # @group Queries
108
+
58
109
  # Does self fall on a weekend?
110
+ # @return [Boolean]
59
111
  def weekend?
60
112
  saturday? || sunday?
61
113
  end
62
114
 
115
+ # :category: Queries
116
+
63
117
  # Does self fall on a weekday?
118
+ # @return [Boolean]
64
119
  def weekday?
65
120
  !weekend?
66
121
  end
67
122
 
68
- # Self's calendar half: 1 or 2
123
+ # :category: Queries
124
+
125
+ # Self's calendar "half" by analogy to calendar quarters: 1 or 2, depending
126
+ # on whether the date falls in the first or second half of the calendar
127
+ # year.
128
+ # @return [1, 2]
69
129
  def half
70
130
  case month
71
131
  when (1..6)
@@ -75,7 +135,11 @@ module FatCore
75
135
  end
76
136
  end
77
137
 
78
- # Self's calendar quarter: 1, 2, 3, or 4
138
+ # :category: Queries
139
+
140
+ # Self's calendar quarter: 1, 2, 3, or 4, depending on which calendar quarter
141
+ # the date falls in.
142
+ # @return [1, 2, 3, 4]
79
143
  def quarter
80
144
  case month
81
145
  when (1..3)
@@ -89,7 +153,212 @@ module FatCore
89
153
  end
90
154
  end
91
155
 
156
+ # :category: Queries
157
+
158
+ # Return whether the date falls on the first day of a year.
159
+ # @return [Boolean]
160
+ def beginning_of_year?
161
+ beginning_of_year == self
162
+ end
163
+
164
+ # :category: Queries
165
+
166
+ # Return whether the date falls on the last day of a year.
167
+ # @return [Boolean]
168
+ def end_of_year?
169
+ end_of_year == self
170
+ end
171
+
172
+ # :category: Queries
173
+
174
+ # Return whether the date falls on the first day of a half-year.
175
+ # @return [Boolean]
176
+ def beginning_of_half?
177
+ beginning_of_half == self
178
+ end
179
+
180
+ # :category: Queries
181
+
182
+ # Return whether the date falls on the last day of a half-year.
183
+ # @return [Boolean]
184
+ def end_of_half?
185
+ end_of_half == self
186
+ end
187
+
188
+ # :category: Queries
189
+
190
+ # Return whether the date falls on the first day of a calendar quarter.
191
+ # @return [Boolean]
192
+ def beginning_of_quarter?
193
+ beginning_of_quarter == self
194
+ end
195
+
196
+ # :category: Queries
197
+
198
+ # Return whether the date falls on the last day of a calendar quarter.
199
+ # @return [Boolean]
200
+ def end_of_quarter?
201
+ end_of_quarter == self
202
+ end
203
+
204
+ # :category: Queries
205
+
206
+ # Return whether the date falls on the first day of a calendar bi-monthly
207
+ # period, i.e., the beginning of an odd-numbered month.
208
+ # @return [Boolean]
209
+ def beginning_of_bimonth?
210
+ month.odd? && beginning_of_month == self
211
+ end
212
+
213
+ # :category: Queries
214
+
215
+ # Return whether the date falls on the last day of a calendar bi-monthly
216
+ # period, i.e., the end of an even-numbered month.
217
+ # @return [Boolean]
218
+ def end_of_bimonth?
219
+ month.even? && end_of_month == self
220
+ end
221
+
222
+ # :category: Queries
223
+
224
+ # Return whether the date falls on the first day of a calendar month.
225
+ # @return [Boolean]
226
+ def beginning_of_month?
227
+ beginning_of_month == self
228
+ end
229
+
230
+ # :category: Queries
231
+
232
+ # Return whether the date falls on the last day of a calendar month.
233
+ # @return [Boolean]
234
+ def end_of_month?
235
+ end_of_month == self
236
+ end
237
+
238
+ # :category: Queries
239
+
240
+ # Return whether the date falls on the first day of a calendar semi-monthly
241
+ # period, i.e., on the 1st or 15th of a month.
242
+ # @return [Boolean]
243
+ def beginning_of_semimonth?
244
+ beginning_of_semimonth == self
245
+ end
246
+
247
+ # :category: Queries
248
+
249
+ # Return whether the date falls on the last day of a calendar semi-monthly
250
+ # period, i.e., on the 14th or the last day of a month.
251
+ # @return [Boolean]
252
+ def end_of_semimonth?
253
+ end_of_semimonth == self
254
+ end
255
+
256
+ # :category: Queries
257
+
258
+ # Return whether the date falls on the first day of a commercial bi-week,
259
+ # i.e., on /Monday/ in a commercial week that is an odd-numbered week. From
260
+ # ::Date: "The calendar week is a seven day period within a calendar year,
261
+ # starting on a Monday and identified by its ordinal number within the year;
262
+ # the first calendar week of the year is the one that includes the first
263
+ # Thursday of that year. In the Gregorian calendar, this is equivalent to
264
+ # the week which includes January 4."
265
+ # @return [Boolean]
266
+ def beginning_of_biweek?
267
+ beginning_of_biweek == self
268
+ end
269
+
270
+ # :category: Queries
271
+
272
+ # Return whether the date falls on the last day of a commercial bi-week,
273
+ # i.e., on /Sunday/ in a commercial week that is an even-numbered week. From
274
+ # ::Date: "The calendar week is a seven day period within a calendar year,
275
+ # starting on a Monday and identified by its ordinal number within the year;
276
+ # the first calendar week of the year is the one that includes the first
277
+ # Thursday of that year. In the Gregorian calendar, this is equivalent to
278
+ # the week which includes January 4."
279
+ # @return [Boolean]
280
+ def end_of_biweek?
281
+ end_of_biweek == self
282
+ end
283
+
284
+ # :category: Queries
285
+
286
+ # Return whether the date falls on the first day of a commercial week, i.e.,
287
+ # on /Monday/ in a commercial week. From ::Date: "The calendar week is a seven
288
+ # day period within a calendar year, starting on a Monday and identified by
289
+ # its ordinal number within the year; the first calendar week of the year is
290
+ # the one that includes the first Thursday of that year. In the Gregorian
291
+ # calendar, this is equivalent to the week which includes January 4."
292
+ # @return [Boolean]
293
+ def beginning_of_week?
294
+ beginning_of_week == self
295
+ end
296
+
297
+ # :category: Queries
298
+
299
+ # Return whether the date falls on the first day of a commercial week, i.e.,
300
+ # on /Sunday/ in a commercial week. From ::Date: "The calendar week is a seven
301
+ # day period within a calendar year, starting on a Monday and identified by
302
+ # its ordinal number within the year; the first calendar week of the year is
303
+ # the one that includes the first Thursday of that year. In the Gregorian
304
+ # calendar, this is equivalent to the week which includes January 4."
305
+ # @return [Boolean]
306
+ def end_of_week?
307
+ end_of_week == self
308
+ end
309
+
310
+ # Return whether this date falls within a period of *less* than six months
311
+ # from the date `d` using the *Stella v. Graham Page Motors* convention that
312
+ # "less" than six months is true only if this date falls within the range of
313
+ # dates 2 days after date six months before and 2 days before the date six
314
+ # months after the date `d`.
315
+ #
316
+ # @param d [::Date] the middle of the six-month range
317
+ # @return [Boolean]
318
+ def within_6mos_of?(d)
319
+ # ::Date 6 calendar months before self
320
+ start_date = self - 6.months + 2.days
321
+ end_date = self + 6.months - 2.days
322
+ (start_date..end_date).cover?(d)
323
+ end
324
+
325
+ # Return whether this date is Easter Sunday for the year in which it falls
326
+ # according to the Western Church. A few holidays key off this date as
327
+ # "moveable feasts."
328
+ #
329
+ # @return [Boolean]
330
+ def easter?
331
+ # Am I Easter?
332
+ self == easter_this_year
333
+ end
334
+
335
+ # Return whether this date is the `n`th weekday `wday` of the given `month` in
336
+ # this date's year.
337
+ #
338
+ # @param n [Integer] number of wday in month, if negative count from end of
339
+ # the month
340
+ # @param wday [Integer] day of week, 0 is Sunday, 1 Monday, etc.
341
+ # @param month [Integer] the month number, 1 is January, 2 is February, etc.
342
+ # @return [Boolean]
343
+ def nth_wday_in_month?(n, wday, month)
344
+ # Is self the nth weekday in the given month of its year?
345
+ # If n is negative, count from last day of month
346
+ self == ::Date.nth_wday_in_year_month(n, wday, year, month)
347
+ end
348
+
349
+ # :category: Relative ::Dates
350
+ # @group Relative ::Dates
351
+
352
+ # Predecessor of self, opposite of `#succ`.
353
+ # @return [::Date]
354
+ def pred
355
+ self - 1.day
356
+ end
357
+
358
+ # Note: the ::Date class already has a #succ method.
359
+
92
360
  # The date that is the first day of the half-year in which self falls.
361
+ # @return [::Date]
93
362
  def beginning_of_half
94
363
  if month > 9
95
364
  (beginning_of_quarter - 15).beginning_of_quarter
@@ -100,7 +369,10 @@ module FatCore
100
369
  end
101
370
  end
102
371
 
372
+ # :category: Relative ::Dates
373
+
103
374
  # The date that is the last day of the half-year in which self falls.
375
+ # @return [::Date]
104
376
  def end_of_half
105
377
  if month < 4
106
378
  (end_of_quarter + 15).end_of_quarter
@@ -111,10 +383,13 @@ module FatCore
111
383
  end
112
384
  end
113
385
 
386
+ # :category: Relative ::Dates
387
+
114
388
  # The date that is the first day of the bimonth in which self
115
389
  # falls. A 'bimonth' is a two-month calendar period beginning on the
116
390
  # first day of the odd-numbered months. E.g., 2014-01-01 to
117
391
  # 2014-02-28 is the first bimonth of 2014.
392
+ # @return [::Date]
118
393
  def beginning_of_bimonth
119
394
  if month.odd?
120
395
  beginning_of_month
@@ -123,10 +398,13 @@ module FatCore
123
398
  end
124
399
  end
125
400
 
401
+ # :category: Relative ::Dates
402
+
126
403
  # The date that is the last day of the bimonth in which self falls.
127
404
  # A 'bimonth' is a two-month calendar period beginning on the first
128
405
  # day of the odd-numbered months. E.g., 2014-01-01 to 2014-02-28 is
129
406
  # the first bimonth of 2014.
407
+ # @return [::Date]
130
408
  def end_of_bimonth
131
409
  if month.odd?
132
410
  (self + 1.month).end_of_month
@@ -135,10 +413,13 @@ module FatCore
135
413
  end
136
414
  end
137
415
 
416
+ # :category: Relative ::Dates
417
+
138
418
  # The date that is the first day of the semimonth in which self
139
419
  # falls. A semimonth is a calendar period beginning on the 1st or
140
420
  # 16th of each month and ending on the 15th or last day of the month
141
421
  # respectively. So each year has exactly 24 semimonths.
422
+ # @return [::Date]
142
423
  def beginning_of_semimonth
143
424
  if day >= 16
144
425
  ::Date.new(year, month, 16)
@@ -147,10 +428,13 @@ module FatCore
147
428
  end
148
429
  end
149
430
 
431
+ # :category: Relative ::Dates
432
+
150
433
  # The date that is the last day of the semimonth in which self
151
434
  # falls. A semimonth is a calendar period beginning on the 1st or
152
435
  # 16th of each month and ending on the 15th or last day of the month
153
436
  # respectively. So each year has exactly 24 semimonths.
437
+ # @return [::Date]
154
438
  def end_of_semimonth
155
439
  if day <= 15
156
440
  ::Date.new(year, month, 15)
@@ -159,8 +443,13 @@ module FatCore
159
443
  end
160
444
  end
161
445
 
162
- # Note: we use a Monday start of the week in the next two methods because
163
- # commercial week counting assumes a Monday start.
446
+ # :category: Relative ::Dates
447
+
448
+ # Return the date that is the first day of the commercial biweek in which
449
+ # self falls. A biweek is a period of two commercial weeks starting with an
450
+ # odd-numbered week and with each week starting in Monday and ending on
451
+ # Sunday.
452
+ # @return [::Date]
164
453
  def beginning_of_biweek
165
454
  if cweek.odd?
166
455
  beginning_of_week(:monday)
@@ -169,6 +458,13 @@ module FatCore
169
458
  end
170
459
  end
171
460
 
461
+ # :category: Relative ::Dates
462
+
463
+ # Return the date that is the last day of the commercial biweek in which
464
+ # self falls. A biweek is a period of two commercial weeks starting with an
465
+ # odd-numbered week and with each week starting in Monday and ending on
466
+ # Sunday. So this will always return a Sunday in an even-numbered week.
467
+ # @return [::Date]
172
468
  def end_of_biweek
173
469
  if cweek.odd?
174
470
  (self + 1.week).end_of_week(:monday)
@@ -177,74 +473,210 @@ module FatCore
177
473
  end
178
474
  end
179
475
 
180
- def beginning_of_year?
181
- beginning_of_year == self
182
- end
183
-
184
- def end_of_year?
185
- end_of_year == self
476
+ # Return the date that is +n+ calendar halves after this date, where a
477
+ # calendar half is a period of 6 months.
478
+ #
479
+ # @param n [Integer] number of halves to advance, can be negative
480
+ # @return [::Date] new date n halves after this date
481
+ def next_half(n = 1)
482
+ n = n.floor
483
+ return self if n.zero?
484
+ next_month(n * 6)
186
485
  end
187
486
 
188
- def beginning_of_half?
189
- beginning_of_half == self
487
+ # Return the date that is +n+ calendar halves before this date, where a
488
+ # calendar half is a period of 6 months.
489
+ #
490
+ # @param n [Integer] number of halves to retreat, can be negative
491
+ # @return [::Date] new date n halves before this date
492
+ def prior_half(n = 1)
493
+ next_half(-n)
190
494
  end
191
495
 
192
- def end_of_half?
193
- end_of_half == self
496
+ # Return the date that is +n+ calendar quarters after this date, where a
497
+ # calendar quarter is a period of 3 months.
498
+ #
499
+ # @param n [Integer] number of quarters to advance, can be negative
500
+ # @return [::Date] new date n quarters after this date
501
+ def next_quarter(n = 1)
502
+ n = n.floor
503
+ return self if n.zero?
504
+ next_month(n * 3)
194
505
  end
195
506
 
196
- def beginning_of_quarter?
197
- beginning_of_quarter == self
507
+ # Return the date that is +n+ calendar quarters before this date, where a
508
+ # calendar quarter is a period of 3 months.
509
+ #
510
+ # @param n [Integer] number of quarters to retreat, can be negative
511
+ # @return [::Date] new date n quarters after this date
512
+ def prior_quarter(n = 1)
513
+ next_quarter(-n)
198
514
  end
199
515
 
200
- def end_of_quarter?
201
- end_of_quarter == self
516
+ # Return the date that is +n+ calendar bimonths after this date, where a
517
+ # calendar bimonth is a period of 2 months.
518
+ #
519
+ # @param n [Integer] number of bimonths to advance, can be negative
520
+ # @return [::Date] new date n bimonths after this date
521
+ def next_bimonth(n = 1)
522
+ n = n.floor
523
+ return self if n.zero?
524
+ next_month(n * 2)
202
525
  end
203
526
 
204
- def beginning_of_bimonth?
205
- month.odd? && beginning_of_month == self
527
+ # Return the date that is +n+ calendar bimonths before this date, where a
528
+ # calendar bimonth is a period of 2 months.
529
+ #
530
+ # @param n [Integer] number of bimonths to retreat, can be negative
531
+ # @return [::Date] new date n bimonths before this date
532
+ def prior_bimonth(n = 1)
533
+ next_bimonth(-n)
206
534
  end
207
535
 
208
- def end_of_bimonth?
209
- month.even? && end_of_month == self
536
+ # Return the date that is +n+ semimonths after this date. Each semimonth begins
537
+ # on the 1st or 16th of the month, and advancing one semimonth from the first
538
+ # half of a month means to go as far past the 16th as the current date is past
539
+ # the 1st; advancing one semimonth from the second half of a month means to go
540
+ # as far into the next month past the 1st as the current date is past the
541
+ # 16th, but never past the 15th of the next month.
542
+ #
543
+ # @param n [Integer] number of semimonths to advance, can be negative
544
+ # @return [::Date] new date n semimonths after this date
545
+ def next_semimonth(n = 1)
546
+ n = n.floor
547
+ return self if n.zero?
548
+ factor = n.negative? ? -1 : 1
549
+ n = n.abs
550
+ if n.even?
551
+ next_month(n / 2)
552
+ else
553
+ # Advance or retreat one semimonth
554
+ next_sm =
555
+ if day == 1
556
+ if factor.positive?
557
+ beginning_of_month + 16.days
558
+ else
559
+ prior_month.beginning_of_month + 16.days
560
+ end
561
+ elsif day == 16
562
+ if factor.positive?
563
+ next_month.beginning_of_month
564
+ else
565
+ beginning_of_month
566
+ end
567
+ elsif day < 16
568
+ # In the first half of the month (the 2nd to the 15th), go as far past
569
+ # the 16th as the date is past the 1st. Thus, as many as 14 days past
570
+ # the 16th, so at most to the 30th of the month unless there are less
571
+ # than 30 days in the month, then go to the end of the month.
572
+ if factor.positive?
573
+ [beginning_of_month + 16.days + (day - 1).days, end_of_month].min
574
+ else
575
+ [prior_month.beginning_of_month + 16.days + (day - 1).days,
576
+ prior_month.end_of_month].min
577
+ end
578
+ else
579
+ # In the second half of the month (17th to the 31st), go as many
580
+ # days into the next month as we are past the 16th. Thus, as many as
581
+ # 15 days. But we don't want to go past the first half of the next
582
+ # month, so we only go so far as the 15th of the next month.
583
+ # ::Date.parse('2015-02-18').next_semimonth should be the 3rd of the
584
+ # following month.
585
+ if factor.positive?
586
+ next_month.beginning_of_month + [(day - 16), 15].min
587
+ else
588
+ beginning_of_month + [(day - 16), 15].min
589
+ end
590
+ end
591
+ n -= 1
592
+ # Now that n is even, advance (or retreat) n / 2 months unless we're done.
593
+ if n >= 2
594
+ next_sm.next_month(factor * n / 2)
595
+ else
596
+ next_sm
597
+ end
598
+ end
210
599
  end
211
600
 
212
- def beginning_of_month?
213
- beginning_of_month == self
601
+ # Return the date that is +n+ semimonths before this date. Each semimonth
602
+ # begins on the 1st or 15th of the month, and retreating one semimonth from
603
+ # the first half of a month means to go as far past the 15th of the prior
604
+ # month as the current date is past the 1st; retreating one semimonth from the
605
+ # second half of a month means to go as far past the 1st of the current month
606
+ # as the current date is past the 15th, but never past the 14th of the the
607
+ # current month.
608
+ #
609
+ # @param n [Integer] number of semimonths to retreat, can be negative
610
+ # @return [::Date] new date n semimonths before this date
611
+ def prior_semimonth(n = 1)
612
+ next_semimonth(-n)
214
613
  end
215
614
 
216
- def end_of_month?
217
- end_of_month == self
615
+ # Return the date that is +n+ biweeks after this date where each biweek is 14
616
+ # days.
617
+ #
618
+ # @param n [Integer] number of biweeks to advance, can be negative
619
+ # @return [::Date] new date n biweeks after this date
620
+ def next_biweek(n = 1)
621
+ n = n.floor
622
+ return self if n.zero?
623
+ self + (14 * n)
218
624
  end
219
625
 
220
- def beginning_of_semimonth?
221
- beginning_of_semimonth == self
626
+ # Return the date that is +n+ biweeks before this date where each biweek is 14
627
+ # days.
628
+ #
629
+ # @param n [Integer] number of biweeks to retreat, can be negative
630
+ # @return [::Date] new date n biweeks before this date
631
+ def prior_biweek(n = 1)
632
+ next_biweek(-n)
222
633
  end
223
634
 
224
- def end_of_semimonth?
225
- end_of_semimonth == self
635
+ # Return the date that is +n+ weeks after this date where each week is 7 days.
636
+ # This is different from the #next_week method in active_support, which
637
+ # goes to the first day of the week in the next week and does not take an
638
+ # argument +n+ to go multiple weeks.
639
+ #
640
+ # @param n [Integer] number of weeks to advance
641
+ # @return [::Date] new date n weeks after this date
642
+ def next_week(n = 1)
643
+ n = n.floor
644
+ return self if n.zero?
645
+ self + (7 * n)
226
646
  end
227
647
 
228
- def beginning_of_biweek?
229
- beginning_of_biweek == self
648
+ # Return the date that is +n+ weeks before this date where each week is 7
649
+ # days.
650
+ #
651
+ # @param n [Integer] number of weeks to retreat
652
+ # @return [::Date] new date n weeks from this date
653
+ def prior_week(n)
654
+ next_week(-n)
230
655
  end
231
656
 
232
- def end_of_biweek?
233
- end_of_biweek == self
234
- end
657
+ # NOTE: #next_day is defined in active_support.
235
658
 
236
- def beginning_of_week?
237
- beginning_of_week == self
659
+ # Return the date that is +n+ weeks before this date where each week is 7
660
+ # days.
661
+ #
662
+ # @param n [Integer] number of days to retreat
663
+ # @return [::Date] new date n days before this date
664
+ def prior_day(n)
665
+ next_day(-n)
238
666
  end
239
667
 
240
- def end_of_week?
241
- end_of_week == self
242
- end
668
+ # :category: Relative ::Dates
243
669
 
244
- def add_chunk(chunk)
670
+ # Return the date that is n chunks later than self.
671
+ #
672
+ # @param chunk [Symbol] one of +:year+, +:half+, +:quarter+, +:bimonth+,
673
+ # +:month+, +:semimonth+, +:biweek+, +:week+, or +:day+.
674
+ # @param n [Integer] the number of chunks to add, can be negative
675
+ # @return [::Date] the date n chunks from this date
676
+ def add_chunk(chunk, n = 1)
245
677
  case chunk
246
678
  when :year
247
- next_year
679
+ next_year(n)
248
680
  when :half
249
681
  next_month(6)
250
682
  when :quarter
@@ -252,22 +684,29 @@ module FatCore
252
684
  when :bimonth
253
685
  next_month(2)
254
686
  when :month
255
- next_month
687
+ next_month(n)
256
688
  when :semimonth
257
- self + 15.days
689
+ next_semimonth(n)
258
690
  when :biweek
259
- self + 14.days
691
+ next_biweek(n)
260
692
  when :week
261
- self + 7.days
693
+ next_week(n)
262
694
  when :day
263
- self + 1.days
695
+ next_day(n)
264
696
  else
265
697
  raise ArgumentError, "add_chunk unknown chunk: '#{chunk}'"
266
698
  end
267
699
  end
268
700
 
269
- def beginning_of_chunk(sym)
270
- case sym
701
+ # Return the date that is the beginning of the +chunk+ in which this date
702
+ # falls.
703
+ #
704
+ # @param chunk [Symbol] one of +:year+, +:half+, +:quarter+, +:bimonth+,
705
+ # +:month+, +:semimonth+, +:biweek+, +:week+, or +:day+.
706
+ # @return [::Date] the first date in the chunk-sized period in which this date
707
+ # falls
708
+ def beginning_of_chunk(chunk)
709
+ case chunk
271
710
  when :year
272
711
  beginning_of_year
273
712
  when :half
@@ -287,12 +726,19 @@ module FatCore
287
726
  when :day
288
727
  self
289
728
  else
290
- raise ArgumentError, "unknown chunk sym: '#{sym}'"
729
+ raise ArgumentError, "unknown chunk sym: '#{chunk}'"
291
730
  end
292
731
  end
293
732
 
294
- def end_of_chunk(sym)
295
- case sym
733
+ # Return the date that is the end of the +chunk+ in which this date
734
+ # falls.
735
+ #
736
+ # @param chunk [Symbol] one of +:year+, +:half+, +:quarter+, +:bimonth+,
737
+ # +:month+, +:semimonth+, +:biweek+, +:week+, or +:day+.
738
+ # @return [::Date] the first date in the chunk-sized period in which this date
739
+ # falls
740
+ def end_of_chunk(chunk)
741
+ case chunk
296
742
  when :year
297
743
  end_of_year
298
744
  when :half
@@ -312,45 +758,157 @@ module FatCore
312
758
  when :day
313
759
  self
314
760
  else
315
- raise ArgumentError, "unknown chunk sym: '#{sym}'"
761
+ raise ArgumentError, "unknown chunk: '#{chunk}'"
316
762
  end
317
763
  end
318
764
 
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
325
-
765
+ # Return the date for Easter in the Western Church for the year in which this
766
+ # date falls.
767
+ #
768
+ # @return [::Date]
326
769
  def easter_this_year
327
770
  # Return the date of Easter in self's year
328
771
  ::Date.easter(year)
329
772
  end
330
773
 
331
- def easter?
332
- # Am I Easter?
333
- self == easter_this_year
334
- end
774
+ # @group Federal Holidays and Workdays
335
775
 
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)
340
- end
341
-
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
776
+ # Holidays decreed by Presidential proclamation
349
777
  FED_DECREED_HOLIDAYS =
350
778
  [
779
+ # Obama decree extra day before Christmas See
780
+ # http://www.whitehouse.gov/the-press-office/2012/12/21
351
781
  ::Date.parse('2012-12-24')
352
782
  ].freeze
353
783
 
784
+ # Presidential funeral since JFK
785
+ PRESIDENTIAL_FUNERALS = [
786
+ # JKF Funeral
787
+ ::Date.parse('1963-11-25'),
788
+ # DWE Funeral
789
+ ::Date.parse('1969-03-31'),
790
+ # HST Funeral
791
+ ::Date.parse('1972-12-28'),
792
+ # LBJ Funeral
793
+ ::Date.parse('1973-01-25'),
794
+ # RMN Funeral
795
+ ::Date.parse('1994-04-27'),
796
+ # RWR Funeral
797
+ ::Date.parse('2004-06-11'),
798
+ # GTF Funeral
799
+ ::Date.parse('2007-01-02')
800
+ ]
801
+
802
+ # Return whether this date is a United States federal holiday.
803
+ #
804
+ # Calculations for Federal holidays are based on 5 USC 6103, include all
805
+ # weekends, Presidential funerals, and holidays decreed executive orders.
806
+ #
807
+ # @return [Boolean]
808
+ def fed_holiday?
809
+ # All Saturdays and Sundays are "holidays"
810
+ return true if weekend?
811
+
812
+ # Some days are holidays by executive decree
813
+ return true if FED_DECREED_HOLIDAYS.include?(self)
814
+
815
+ # Presidential funerals
816
+ return true if PRESIDENTIAL_FUNERALS.include?(self)
817
+
818
+ # Is self a fixed holiday
819
+ return true if fed_fixed_holiday? || fed_moveable_feast?
820
+
821
+ if friday? && month == 12 && day == 26
822
+ # If Christmas falls on a Thursday, apparently, the Friday after is
823
+ # treated as a holiday as well. See 2003, 2008, for example.
824
+ true
825
+ elsif friday?
826
+ # A Friday is a holiday if a fixed-date holiday
827
+ # would fall on the following Saturday
828
+ (self + 1).fed_fixed_holiday? || (self + 1).fed_moveable_feast?
829
+ elsif monday?
830
+ # A Monday is a holiday if a fixed-date holiday
831
+ # would fall on the preceding Sunday
832
+ (self - 1).fed_fixed_holiday? || (self - 1).fed_moveable_feast?
833
+ elsif (year % 4 == 1) && year > 1965 && mon == 1 && mday == 20
834
+ # Inauguration Day after 1965 is a federal holiday, but if it falls on a
835
+ # Sunday, the following Monday is observed, but if it falls on a
836
+ # Saturday, the prior Friday is /not/ observed. So, we can't just count
837
+ # this as a regular fixed holiday.
838
+ true
839
+ elsif monday? && (year % 4 == 1) && year > 1965 && mon == 1 && mday == 21
840
+ # Inauguration Day after 1965 is a federal holiday, but if it falls on a
841
+ # Sunday, the following Monday is observed, but if it falls on a
842
+ # Saturday, the prior Friday is /not/ observed.
843
+ true
844
+ else
845
+ false
846
+ end
847
+ end
848
+
849
+ # Return whether this date is a date on which the US federal government is
850
+ # open for business. It is the opposite of #fed_holiday?
851
+ #
852
+ # @return [Boolean]
853
+ def fed_workday?
854
+ !fed_holiday?
855
+ end
856
+
857
+ # :category: Queries
858
+
859
+ # Return the date that is n federal workdays after or before (if n < 0) this
860
+ # date.
861
+ #
862
+ # @param n [Integer] number of federal workdays to add to this date
863
+ # @return [::Date]
864
+ def add_fed_workdays(n)
865
+ d = dup
866
+ return d if n.zero?
867
+ incr = n.negative? ? -1 : 1
868
+ n = n.abs
869
+ while n.positive?
870
+ d += incr
871
+ n -= 1 if d.fed_workday?
872
+ end
873
+ d
874
+ end
875
+
876
+ # Return the next federal workday after this date. The date returned is always
877
+ # a date at least one day after this date, never this date.
878
+ #
879
+ # @return [::Date]
880
+ def next_fed_workday
881
+ add_fed_workdays(1)
882
+ end
883
+
884
+ # Return the last federal workday before this date. The date returned is always
885
+ # a date at least one day before this date, never this date.
886
+ #
887
+ # @return [::Date]
888
+ def prior_fed_workday
889
+ add_fed_workdays(-1)
890
+ end
891
+
892
+ # Return this date if its a federal workday, otherwise skip forward to the
893
+ # first later federal workday.
894
+ #
895
+ # @return [::Date]
896
+ def next_until_fed_workday
897
+ date = dup
898
+ date += 1 until date.fed_workday?
899
+ date
900
+ end
901
+
902
+ # Return this if its a federal workday, otherwise skip back to the first prior
903
+ # federal workday.
904
+ def prior_until_fed_workday
905
+ date = dup
906
+ date -= 1 until date.fed_workday?
907
+ date
908
+ end
909
+
910
+ protected
911
+
354
912
  def fed_fixed_holiday?
355
913
  # Fixed-date holidays on weekdays
356
914
  if mon == 1 && mday == 1
@@ -399,52 +957,144 @@ module FatCore
399
957
  end
400
958
  end
401
959
 
402
- def fed_holiday?
960
+ # @group NYSE Holidays and Workdays
961
+
962
+ # :category: Queries
963
+
964
+ public
965
+
966
+ # Returns whether this date is one on which the NYSE was or is expected to be
967
+ # closed for business.
968
+ #
969
+ # Calculations for NYSE holidays are from Rule 51 and supplementary materials
970
+ # for the Rules of the New York Stock Exchange, Inc.
971
+ #
972
+ # * General Rule 1: if a regular holiday falls on Saturday, observe it on the preceding Friday.
973
+ # * General Rule 2: if a regular holiday falls on Sunday, observe it on the following Monday.
974
+ #
975
+ # These are the regular holidays:
976
+ #
977
+ # * New Year's Day, January 1.
978
+ # * Birthday of Martin Luther King, Jr., the third Monday in January.
979
+ # * Washington's Birthday, the third Monday in February.
980
+ # * Good Friday Friday before Easter Sunday. NOTE: this is not a fed holiday
981
+ # * Memorial Day, the last Monday in May.
982
+ # * Independence Day, July 4.
983
+ # * Labor Day, the first Monday in September.
984
+ # * Thanksgiving Day, the fourth Thursday in November.
985
+ # * Christmas Day, December 25.
986
+ #
987
+ # Columbus and Veterans days not observed.
988
+ #
989
+ # In addition, there have been several days on which the exchange has been
990
+ # closed for special events such as Presidential funerals, the 9-11 attacks,
991
+ # the paper-work crisis in the 1960's, hurricanes, etc. All of these are
992
+ # considered holidays for purposes of this method.
993
+ #
994
+ # In addition, every weekend is considered a holiday.
995
+ #
996
+ # @return [Boolean]
997
+ def nyse_holiday?
403
998
  # All Saturdays and Sundays are "holidays"
404
999
  return true if weekend?
405
1000
 
406
- # Some days are holidays by executive decree
407
- return true if FED_DECREED_HOLIDAYS.include?(self)
1001
+ # Presidential funerals, observed by NYSE as well.
1002
+ return true if PRESIDENTIAL_FUNERALS.include?(self)
408
1003
 
409
1004
  # Is self a fixed holiday
410
- return true if fed_fixed_holiday? || fed_moveable_feast?
1005
+ return true if nyse_fixed_holiday? || nyse_moveable_feast?
411
1006
 
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?
1007
+ return true if nyse_special_holiday?
1008
+
1009
+ if friday? && (self >= ::Date.parse('1959-07-03'))
1010
+ # A Friday is a holiday if a holiday would fall on the following
1011
+ # Saturday. The rule does not apply if the Friday "ends a monthly or
1012
+ # yearly accounting period." Adopted July 3, 1959. E.g, December 31,
1013
+ # 2010, fell on a Friday, so New Years was on Saturday, but the NYSE
1014
+ # opened because it ended a yearly accounting period. I believe 12/31
1015
+ # is the only date to which the exception can apply since only New
1016
+ # Year's can fall on the first of the month.
1017
+ !end_of_quarter? &&
1018
+ ((self + 1).nyse_fixed_holiday? || (self + 1).nyse_moveable_feast?)
420
1019
  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?
1020
+ # A Monday is a holiday if a holiday would fall on the
1021
+ # preceding Sunday. This has apparently always been the rule.
1022
+ (self - 1).nyse_fixed_holiday? || (self - 1).nyse_moveable_feast?
424
1023
  else
425
1024
  false
426
1025
  end
427
1026
  end
428
1027
 
429
- #######################################################
430
- # Calculations for NYSE holidays
431
- # Rule 51 and supplementary material
432
- #######################################################
1028
+ # Return whether the NYSE is open for trading on this date.
1029
+ #
1030
+ # @return [Boolean]
1031
+ def nyse_workday?
1032
+ !nyse_holiday?
1033
+ end
1034
+ alias trading_day? nyse_workday?
433
1035
 
434
- # Rule: if it falls on Saturday, observe on preceding Friday.
435
- # Rule: if it falls on Sunday, observe on following Monday.
1036
+ # Return the date that is n NYSE trading days after or before (if n < 0) this
1037
+ # date.
436
1038
  #
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.
1039
+ # @param n [Integer] number of NYSE trading days to add to this date
1040
+ # @return [::Date]
1041
+ def add_nyse_workdays(n)
1042
+ d = dup
1043
+ return d if n.zero?
1044
+ incr = n.negative? ? -1 : 1
1045
+ n = n.abs
1046
+ while n.positive?
1047
+ d += incr
1048
+ n -= 1 if d.nyse_workday?
1049
+ end
1050
+ d
1051
+ end
1052
+ alias add_trading_days add_nyse_workdays
1053
+
1054
+ # Return the next NYSE trading day after this date. The date returned is always
1055
+ # a date at least one day after this date, never this date.
1056
+ #
1057
+ # @return [::Date]
1058
+ def next_nyse_workday
1059
+ add_nyse_workdays(1)
1060
+ end
1061
+ alias next_trading_day next_nyse_workday
1062
+
1063
+ # Return the last NYSE trading day before this date. The date returned is always
1064
+ # a date at least one day before this date, never this date.
1065
+ #
1066
+ # @return [::Date]
1067
+ def prior_nyse_workday
1068
+ add_nyse_workdays(-1)
1069
+ end
1070
+ alias prior_trading_day prior_nyse_workday
1071
+
1072
+ # Return this date if its a trading day, otherwise skip forward to the first
1073
+ # later trading day.
1074
+ #
1075
+ # @return [::Date]
1076
+ def next_until_trading_day
1077
+ date = dup
1078
+ date += 1 until date.trading_day?
1079
+ date
1080
+ end
1081
+
1082
+ # Return this date if its a trading day, otherwise skip back to the first prior
1083
+ # trading day.
1084
+ #
1085
+ # @return [::Date]
1086
+ def prior_until_trading_day
1087
+ date = dup
1088
+ date -= 1 until date.trading_day?
1089
+ date
1090
+ end
447
1091
 
1092
+ protected
1093
+
1094
+ # Return whether this date is a fixed holiday for the NYSE, that is, a holiday
1095
+ # that falls on the same date each year.
1096
+ #
1097
+ # @return [Boolean]
448
1098
  def nyse_fixed_holiday?
449
1099
  # Fixed-date holidays
450
1100
  if mon == 1 && mday == 1
@@ -461,6 +1111,12 @@ module FatCore
461
1111
  end
462
1112
  end
463
1113
 
1114
+ # :category: Queries
1115
+
1116
+ # Return whether this date is a non-fixed holiday for the NYSE, that is, a holiday
1117
+ # that can fall on different dates each year, a so-called "moveable feast".
1118
+ #
1119
+ # @return [Boolean]
464
1120
  def nyse_moveable_feast?
465
1121
  # See if today is a "movable feast," all of which are
466
1122
  # rigged to fall on Monday except Thanksgiving
@@ -537,11 +1193,17 @@ module FatCore
537
1193
  end
538
1194
  end
539
1195
 
1196
+ # :category: Queries
1197
+
540
1198
  # They NYSE has closed on several occasions outside its normal holiday
541
1199
  # 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
1200
+ # day is not counted. See http://www1.nyse.com/pdfs/closings.pdf. Return
1201
+ # whether this date is one of those special closings.
1202
+ #
1203
+ # @return [Boolean]
543
1204
  def nyse_special_holiday?
544
1205
  return false unless self > ::Date.parse('1960-01-01')
1206
+ return true if PRESIDENTIAL_FUNERALS.include?(self)
545
1207
  case self
546
1208
  when ::Date.parse('1961-05-29')
547
1209
  # Day before Decoaration Day
@@ -568,33 +1230,17 @@ module FatCore
568
1230
  when ::Date.parse('1969-02-10')
569
1231
  # Heavy snow
570
1232
  true
571
- when ::Date.parse('1969-05-31')
572
- # Eisenhower Funeral
573
- true
574
1233
  when ::Date.parse('1969-07-21')
575
1234
  # Moon landing
576
1235
  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
1236
  when ::Date.parse('1977-07-14')
584
1237
  # Electrical blackout NYC
585
1238
  true
586
1239
  when ::Date.parse('1985-09-27')
587
1240
  # Hurricane Gloria
588
1241
  true
589
- when ::Date.parse('1994-04-27')
590
- # Nixon Funeral
591
- true
592
1242
  when (::Date.parse('2001-09-11')..::Date.parse('2001-09-14'))
593
1243
  # 9-11 Attacks
594
- a = a
595
- true
596
- when (::Date.parse('2004-06-11')..::Date.parse('2001-09-14'))
597
- # Reagan Funeral
598
1244
  true
599
1245
  when ::Date.parse('2007-01-02')
600
1246
  # Observance death of President Ford
@@ -607,123 +1253,29 @@ module FatCore
607
1253
  end
608
1254
  end
609
1255
 
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
636
- end
637
-
638
- def fed_workday?
639
- !fed_holiday?
640
- end
641
-
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
658
-
659
- def next_fed_workday
660
- add_fed_business_days(1)
661
- end
662
-
663
- def prior_fed_workday
664
- add_fed_business_days(-1)
665
- end
666
-
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
679
-
680
- def next_nyse_workday
681
- add_nyse_business_days(1)
682
- end
683
- alias next_trading_day next_nyse_workday
684
-
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
697
-
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
1256
  module ClassMethods
707
- # Convert a string with an American style date into a Date object
1257
+ # @group Parsing
1258
+ #
1259
+ # Convert a string +str+ with an American style date into a ::Date object
708
1260
  #
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
1261
+ # An American style date is of the form `MM/DD/YYYY`, that is it places the
1262
+ # month first, then the day of the month, and finally the year. The European
1263
+ # convention is typically to place the day of the month first, `DD/MM/YYYY`.
1264
+ # A date found in the wild can be ambiguous, e.g. 3/5/2014, but a date
713
1265
  # 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.
1266
+ # method. Both the month and the day can be a single digit. The year can be
1267
+ # either 2 or 4 digits, and if given as 2 digits, it adds 2000 to it to give
1268
+ # the year.
717
1269
  #
718
1270
  # @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
1271
+ # ::Date.parse_american('9/11/2001') #=> ::Date(2011, 9, 11)
1272
+ # ::Date.parse_american('9/11/01') #=> ::Date(2011, 9, 11)
1273
+ # ::Date.parse_american('9/11/1') #=> ArgumentError
722
1274
  #
723
- # @param str [#to_s] a stringling of the form MM/DD/YYYY
724
- # @return [Date] the date represented by the string paramenter.
1275
+ # @param str [String, #to_s] a stringling of the form MM/DD/YYYY
1276
+ # @return [::Date] the date represented by the str paramenter.
725
1277
  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}
1278
+ unless str.to_s =~ %r{\A\s*(\d\d?)\s*/\s*(\d\d?)\s*/\s*((\d\d)?\d\d)\s*\z}
727
1279
  raise ArgumentError, "date string must be of form 'MM?/DD?/YY(YY)?'"
728
1280
  end
729
1281
  year = $3.to_i
@@ -733,31 +1285,58 @@ module FatCore
733
1285
  ::Date.new(year, month, day)
734
1286
  end
735
1287
 
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
1288
+ # Convert a 'period spec' `spec` to a ::Date. A date spec is a short-hand way of
1289
+ # specifying a calendar period either absolutely or relative to the computer
1290
+ # clock. This method returns the first date of that period, when `spec_type`
1291
+ # is set to `:from`, the default, and returns the last date of the period
1292
+ # when `spec_type` is `:to`.
1293
+ #
1294
+ # There are a number of forms the `spec` can take. In each case,
1295
+ # `::Date.parse_spec` returns the first date in the period if `spec_type` is
1296
+ # `:from` and the last date in the period if `spec_type` is `:to`:
740
1297
  #
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.
1298
+ # * `YYYY` is the whole year `YYYY`,
1299
+ # * `YYYY-1H` or `YYYY-H1` is the first calendar half in year `YYYY`,
1300
+ # * `H2` or `2H` is the second calendar half of the current year,
1301
+ # * `YYYY-3Q` or `YYYY-Q3` is the third calendar quarter of year YYYY,
1302
+ # * `Q3` or `3Q` is the third calendar quarter in the current year,
1303
+ # * `YYYY-04` or `YYYY-4` is April, the fourth month of year `YYYY`,
1304
+ # * `4-12` or `04-12` is the 12th of April in the current year,
1305
+ # * `4` or `04` is April in the current year,
1306
+ # * `YYYY-W32` or `YYYY-32W` is the 32nd week in year YYYY,
1307
+ # * `W32` or `32W` is the 32nd week in the current year,
1308
+ # * `YYYY-MM-DD` a particular date, so `:from` and `:to` return the same
1309
+ # date,
1310
+ # * `this_<chunk>` where `<chunk>` is one of `year`, `half`, `quarter`,
1311
+ # `bimonth`, `month`, `semimonth`, `biweek`, `week`, or `day`, the
1312
+ # corresponding calendar period in which the current date falls,
1313
+ # * `last_<chunk>` where `<chunk>` is one of `year`, `half`, `quarter`,
1314
+ # `bimonth`, `month`, `semimonth`, `biweek`, `week`, or `day`, the
1315
+ # corresponding calendar period immediately before the one in which the
1316
+ # current date falls,
1317
+ # * `today` is the same as `this_day`,
1318
+ # * `yesterday` is the same as `last_day`,
1319
+ # * `forever` is the period from ::Date::BOT to ::Date::EOT, essentially all
1320
+ # dates of commercial interest, and
1321
+ # * `never` causes the method to return nil.
1322
+ #
1323
+ # In all of the above example specs, letter used for calendar chunks, `W`,
1324
+ # `Q`, and `H` can be written in lower case as well. Also, you can use `/`
1325
+ # to separate date components instead of `-`.
1326
+ #
1327
+ # @example
1328
+ # ::Date.parse_spec('2012-W32').iso # => "2012-08-06"
1329
+ # ::Date.parse_spec('2012-W32', :to).iso # => "2012-08-12"
1330
+ # ::Date.parse_spec('W32').iso # => "2012-08-06" if executed in 2012
1331
+ # ::Date.parse_spec('W32').iso # => "2012-08-04" if executed in 2014
744
1332
  #
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') # =>
1333
+ # @param spec [String, #to_s] the spec to be interpreted as a calendar period
752
1334
  #
753
- # A spec of the form Q3 or 3Q returns the beginning or end of calendar
754
- # quarters.
755
- # Date.parse_spec('Q3') # =>
1335
+ # @param spec_type [:from, :to] return the first (:from) or last (:to)
1336
+ # date in the spec's period respectively
756
1337
  #
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
1338
+ # @return [::Date] date that is the first (:from) or last (:to) in the period
1339
+ # designated by spec
761
1340
  def parse_spec(spec, spec_type = :from)
762
1341
  spec = spec.to_s.strip
763
1342
  unless [:from, :to].include?(spec_type)
@@ -772,7 +1351,7 @@ module FatCore
772
1351
  when /\AW(\d\d?)\z/, /\A(\d\d?)W\z/
773
1352
  week_num = $1.to_i
774
1353
  if week_num < 1 || week_num > 53
775
- raise ArgumentError, "invalid week number (1-53): 'W#{week_num}'"
1354
+ raise ArgumentError, "invalid week number (1-53): '#{spec}'"
776
1355
  end
777
1356
  if spec_type == :from
778
1357
  ::Date.commercial(today.year, week_num).beginning_of_week
@@ -783,7 +1362,7 @@ module FatCore
783
1362
  year = $1.to_i
784
1363
  week_num = $2.to_i
785
1364
  if week_num < 1 || week_num > 53
786
- raise ArgumentError, "invalid week number (1-53): 'W#{week_num}'"
1365
+ raise ArgumentError, "invalid week number (1-53): '#{spec}'"
787
1366
  end
788
1367
  if spec_type == :from
789
1368
  ::Date.commercial(year, week_num).beginning_of_week
@@ -795,7 +1374,7 @@ module FatCore
795
1374
  year = $1.to_i
796
1375
  quarter = $2.to_i
797
1376
  unless [1, 2, 3, 4].include?(quarter)
798
- raise ArgumentError, "bad date format: #{spec}"
1377
+ raise ArgumentError, "invalid quarter number (1-4): '#{spec}'"
799
1378
  end
800
1379
  month = quarter * 3
801
1380
  if spec_type == :from
@@ -807,6 +1386,9 @@ module FatCore
807
1386
  # Quarter only
808
1387
  this_year = today.year
809
1388
  quarter = $1.to_i
1389
+ unless [1, 2, 3, 4].include?(quarter)
1390
+ raise ArgumentError, "invalid quarter number (1-4): '#{spec}'"
1391
+ end
810
1392
  date = ::Date.new(this_year, quarter * 3, 15)
811
1393
  if spec_type == :from
812
1394
  date.beginning_of_quarter
@@ -818,7 +1400,7 @@ module FatCore
818
1400
  year = $1.to_i
819
1401
  half = $2.to_i
820
1402
  unless [1, 2].include?(half)
821
- raise ArgumentError, "bad date format: #{spec}"
1403
+ raise ArgumentError, "invalid half number: '#{spec}'"
822
1404
  end
823
1405
  month = half * 6
824
1406
  if spec_type == :from
@@ -830,6 +1412,9 @@ module FatCore
830
1412
  # Half only
831
1413
  this_year = today.year
832
1414
  half = $1.to_i
1415
+ unless [1, 2].include?(half)
1416
+ raise ArgumentError, "invalid half number: '#{spec}'"
1417
+ end
833
1418
  date = ::Date.new(this_year, half * 6, 15)
834
1419
  if spec_type == :from
835
1420
  date.beginning_of_half
@@ -838,24 +1423,38 @@ module FatCore
838
1423
  end
839
1424
  when /^(\d\d\d\d)[-\/](\d\d?)*$/
840
1425
  # Year-Month only
1426
+ year = $1.to_i
1427
+ month = $2.to_i
1428
+ unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
1429
+ raise ArgumentError, "invalid month number (1-12): '#{spec}'"
1430
+ end
841
1431
  if spec_type == :from
842
- ::Date.new($1.to_i, $2.to_i, 1)
1432
+ ::Date.new(year, month, 1)
843
1433
  else
844
- ::Date.new($1.to_i, $2.to_i, 1).end_of_month
1434
+ ::Date.new(year, month, 1).end_of_month
845
1435
  end
846
1436
  when /^(\d\d?)[-\/](\d\d?)*$/
847
1437
  # Month-Day only
1438
+ month = $1.to_i
1439
+ day = $2.to_i
1440
+ unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
1441
+ raise ArgumentError, "invalid month number (1-12): '#{spec}'"
1442
+ end
848
1443
  if spec_type == :from
849
- ::Date.new(today.year, $1.to_i, $2.to_i)
1444
+ ::Date.new(today.year, month, day)
850
1445
  else
851
- ::Date.new(today.year, $1.to_i, $2.to_i).end_of_month
1446
+ ::Date.new(today.year, month, day).end_of_month
852
1447
  end
853
1448
  when /\A(\d\d?)\z/
854
1449
  # Month only
1450
+ month = $1.to_i
1451
+ unless [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12].include?(month)
1452
+ raise ArgumentError, "invalid month number (1-12): '#{spec}'"
1453
+ end
855
1454
  if spec_type == :from
856
- ::Date.new(today.year, $1.to_i, 1)
1455
+ ::Date.new(today.year, month, 1)
857
1456
  else
858
- ::Date.new(today.year, $1.to_i, 1).end_of_month
1457
+ ::Date.new(today.year, month, 1).end_of_month
859
1458
  end
860
1459
  when /^(\d\d\d\d)$/
861
1460
  # Year only
@@ -966,11 +1565,15 @@ module FatCore
966
1565
  nil
967
1566
  else
968
1567
  raise ArgumentError, "bad date spec: '#{spec}''"
969
- end # !> previous definition of length was here
1568
+ end
970
1569
  end
971
1570
 
1571
+ # @group Utilities
1572
+
1573
+ # An Array of the number of days in each month indexed by month number,
1574
+ # starting with January = 1, etc.
972
1575
  COMMON_YEAR_DAYS_IN_MONTH = [31, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31,
973
- 30, 31]
1576
+ 30, 31].freeze
974
1577
  def days_in_month(y, m)
975
1578
  raise ArgumentError, 'illegal month number' if m < 1 || m > 12
976
1579
  days = COMMON_YEAR_DAYS_IN_MONTH[m]
@@ -981,9 +1584,14 @@ module FatCore
981
1584
  end
982
1585
  end
983
1586
 
1587
+ # Return the nth weekday in the given month. If n is negative, count from
1588
+ # last day of month.
1589
+ #
1590
+ # @param n [Integer] the ordinal number for the weekday
1591
+ # @param wday [Integer] the weekday of interest with Monday 0 to Sunday 6
1592
+ # @param year [Integer] the year of interest
1593
+ # @param month [Integer] the month of interest with January 1 to December 12
984
1594
  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
1595
  wday = wday.to_i
988
1596
  raise ArgumentError, 'illegal weekday number' if wday < 0 || wday > 6
989
1597
  month = month.to_i
@@ -1013,11 +1621,14 @@ module FatCore
1013
1621
  end
1014
1622
  d
1015
1623
  else
1016
- raise ArgumentError,
1017
- 'Arg 1 to nth_wday_in_month_year cannot be zero'
1624
+ raise ArgumentError, 'Argument n cannot be zero'
1018
1625
  end
1019
1626
  end
1020
1627
 
1628
+ # Return the date of Easter for the Western Church in the given year.
1629
+ #
1630
+ # @param year [Integer] the year of interest
1631
+ # @return [::Date] the date of Easter for year
1021
1632
  def easter(year)
1022
1633
  y = year
1023
1634
  a = y % 19
@@ -1034,13 +1645,17 @@ module FatCore
1034
1645
  end
1035
1646
  end
1036
1647
 
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)
1648
+ public
1649
+
1650
+ # @private
1651
+ def self.included(base)
1652
+ base.extend(ClassMethods)
1042
1653
  end
1043
1654
  end
1044
1655
  end
1045
1656
 
1046
- Date.include(FatCore::Date)
1657
+ class Date
1658
+ include FatCore::Date
1659
+ # @!parse include FatCore::Date
1660
+ # @!parse extend FatCore::Date::ClassMethods
1661
+ end