fat_core 3.0.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +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
|