fat_core 3.0.0 → 4.0.0

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