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 +4 -4
- data/.ruby-version +1 -1
- data/.yardopts +5 -1
- data/README.md +124 -7
- data/Rakefile +17 -1
- data/bin/console +6 -7
- data/bin/easters +1 -1
- data/fat_core.gemspec +3 -2
- data/lib/fat_core/all.rb +1 -4
- data/lib/fat_core/array.rb +4 -1
- data/lib/fat_core/bigdecimal.rb +19 -0
- data/lib/fat_core/date.rb +913 -298
- data/lib/fat_core/hash.rb +98 -15
- data/lib/fat_core/kernel.rb +13 -0
- data/lib/fat_core/nil.rb +16 -2
- data/lib/fat_core/numeric.rb +84 -32
- data/lib/fat_core/range.rb +311 -109
- data/lib/fat_core/string.rb +246 -161
- data/lib/fat_core/symbol.rb +28 -4
- data/lib/fat_core/version.rb +2 -1
- data/spec/lib/{big_decimal_spec.rb → bigdecimal_spec.rb} +1 -1
- data/spec/lib/date_spec.rb +1 -1
- data/spec/lib/numeric_spec.rb +1 -1
- data/spec/lib/range_spec.rb +8 -6
- data/spec/lib/string_spec.rb +72 -58
- data/spec/spec_helper.rb +3 -2
- metadata +9 -10
- data/lib/core_extensions/date/fat_core.rb +0 -6
- data/lib/fat_core/big_decimal.rb +0 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1a7476217f8c6438e15010dcb43a6b3e5a7fbdc3
|
4
|
+
data.tar.gz: 99d35b1291dfa588bef9fbed7185e82c12e6b8ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 003b3ab76e3d11011b7f67504ea0422d1e34cb8c4f32606a4f0b28e216416d13f18d5e713c78ca30200ded965076e2593b75ff80b0002ff35fffc5abdf487cb9
|
7
|
+
data.tar.gz: 901ad06b503df0199ef15ccf46232d2552cd8ae0eb76bbb0b29da4e4e7036a82b274d5be3b07587917116d2450ae608a877714a3dcce0ca4d03b8658083f1ba6
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.3.
|
1
|
+
2.3.3
|
data/.yardopts
CHANGED
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
|
-
|
8
|
-
|
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
|
-
|
20
|
-
|
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
|
4
|
-
require
|
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
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 '
|
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/
|
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'
|
data/lib/fat_core/array.rb
CHANGED
@@ -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
|
-
#
|
11
|
-
#
|
12
|
-
|
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
|
-
#
|
16
|
-
#
|
17
|
-
|
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
|
-
#
|
22
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
163
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
189
|
-
|
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
|
-
|
193
|
-
|
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
|
-
|
197
|
-
|
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
|
-
|
201
|
-
|
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
|
-
|
205
|
-
|
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
|
-
|
209
|
-
|
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
|
-
|
213
|
-
|
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
|
-
|
217
|
-
|
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
|
-
|
221
|
-
|
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
|
-
|
225
|
-
|
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
|
-
|
229
|
-
|
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
|
-
|
233
|
-
end_of_biweek == self
|
234
|
-
end
|
657
|
+
# NOTE: #next_day is defined in active_support.
|
235
658
|
|
236
|
-
|
237
|
-
|
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
|
-
|
241
|
-
end_of_week == self
|
242
|
-
end
|
668
|
+
# :category: Relative ::Dates
|
243
669
|
|
244
|
-
|
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
|
-
|
689
|
+
next_semimonth(n)
|
258
690
|
when :biweek
|
259
|
-
|
691
|
+
next_biweek(n)
|
260
692
|
when :week
|
261
|
-
|
693
|
+
next_week(n)
|
262
694
|
when :day
|
263
|
-
|
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
|
-
|
270
|
-
|
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: '#{
|
729
|
+
raise ArgumentError, "unknown chunk sym: '#{chunk}'"
|
291
730
|
end
|
292
731
|
end
|
293
732
|
|
294
|
-
|
295
|
-
|
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
|
761
|
+
raise ArgumentError, "unknown chunk: '#{chunk}'"
|
316
762
|
end
|
317
763
|
end
|
318
764
|
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
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
|
-
|
332
|
-
# Am I Easter?
|
333
|
-
self == easter_this_year
|
334
|
-
end
|
774
|
+
# @group Federal Holidays and Workdays
|
335
775
|
|
336
|
-
|
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
|
-
|
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
|
-
#
|
407
|
-
return true if
|
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
|
1005
|
+
return true if nyse_fixed_holiday? || nyse_moveable_feast?
|
411
1006
|
|
412
|
-
if
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
#
|
418
|
-
#
|
419
|
-
|
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
|
422
|
-
#
|
423
|
-
(self - 1).
|
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
|
-
#
|
431
|
-
#
|
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
|
-
#
|
435
|
-
#
|
1036
|
+
# Return the date that is n NYSE trading days after or before (if n < 0) this
|
1037
|
+
# date.
|
436
1038
|
#
|
437
|
-
#
|
438
|
-
#
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
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
|
-
#
|
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
|
710
|
-
# month first, then the day of the month, and finally the year.
|
711
|
-
#
|
712
|
-
#
|
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.
|
715
|
-
#
|
716
|
-
#
|
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
|
-
#
|
720
|
-
#
|
721
|
-
#
|
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
|
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
|
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 '
|
737
|
-
# specifying a
|
738
|
-
#
|
739
|
-
#
|
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
|
-
#
|
742
|
-
#
|
743
|
-
#
|
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
|
-
#
|
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
|
-
#
|
754
|
-
#
|
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
|
-
# @
|
758
|
-
#
|
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): '
|
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): '
|
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, "
|
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, "
|
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(
|
1432
|
+
::Date.new(year, month, 1)
|
843
1433
|
else
|
844
|
-
::Date.new(
|
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,
|
1444
|
+
::Date.new(today.year, month, day)
|
850
1445
|
else
|
851
|
-
::Date.new(today.year,
|
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,
|
1455
|
+
::Date.new(today.year, month, 1)
|
857
1456
|
else
|
858
|
-
::Date.new(today.year,
|
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
|
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
|
-
|
1038
|
-
|
1039
|
-
#
|
1040
|
-
def self.included(
|
1041
|
-
|
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
|
1657
|
+
class Date
|
1658
|
+
include FatCore::Date
|
1659
|
+
# @!parse include FatCore::Date
|
1660
|
+
# @!parse extend FatCore::Date::ClassMethods
|
1661
|
+
end
|