fat_core 1.0.3 → 1.2.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/.gitignore +1 -0
- data/.rubocop.yml +3 -0
- data/Rakefile +2 -2
- data/fat_core.gemspec +19 -18
- data/lib/core_extensions/date/fat_core.rb +0 -1
- data/lib/fat_core.rb +5 -1
- data/lib/fat_core/column.rb +197 -0
- data/lib/fat_core/date.rb +192 -132
- data/lib/fat_core/enumerable.rb +1 -1
- data/lib/fat_core/evaluator.rb +43 -0
- data/lib/fat_core/hash.rb +1 -1
- data/lib/fat_core/nil.rb +4 -0
- data/lib/fat_core/numeric.rb +30 -17
- data/lib/fat_core/period.rb +58 -67
- data/lib/fat_core/range.rb +20 -25
- data/lib/fat_core/string.rb +95 -55
- data/lib/fat_core/symbol.rb +12 -14
- data/lib/fat_core/table.rb +515 -0
- data/lib/fat_core/version.rb +2 -2
- data/spec/example_files/goldberg.org +199 -0
- data/spec/example_files/wpcs.csv +92 -0
- data/spec/lib/array_spec.rb +1 -1
- data/spec/lib/date_spec.rb +5 -6
- data/spec/lib/enumerable_spec.rb +1 -1
- data/spec/lib/evaluator_spec.rb +34 -0
- data/spec/lib/hash_spec.rb +5 -5
- data/spec/lib/kernel_spec.rb +3 -3
- data/spec/lib/nil_spec.rb +2 -2
- data/spec/lib/numeric_spec.rb +22 -22
- data/spec/lib/period_spec.rb +11 -12
- data/spec/lib/range_spec.rb +50 -50
- data/spec/lib/string_spec.rb +71 -74
- data/spec/lib/symbol_spec.rb +3 -3
- data/spec/lib/table_spec.rb +659 -0
- data/spec/spec_helper.rb +1 -1
- metadata +28 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e60f8326fe791c7c937d2bf069c82312f1a42d73
|
4
|
+
data.tar.gz: 74562c504f4b8104d8f4d5ad3915076f11475ba7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 16619652648490f700cec9bd6f3f956d501e200e5a5f7606853bf35ec2a5eaa03c40a57dff89aff19d8233893c46b3e11b5cca44bc2c4c57eff873c554acf345
|
7
|
+
data.tar.gz: 2a873b09a9b550c3b58117241134ecc3d9a65e35d77f98fe4621aa52614c1c574a3fbf3ff67f1c5aa4e2eb0f2bcd74f267f5d46329555d2bc8bd6e9eb86124cb
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
data/Rakefile
CHANGED
data/fat_core.gemspec
CHANGED
@@ -4,31 +4,32 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'fat_core/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'fat_core'
|
8
8
|
spec.version = FatCore::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
9
|
+
spec.authors = ['Daniel E. Doherty']
|
10
|
+
spec.email = ['ded@ddoherty.net']
|
11
|
+
spec.summary = 'fat_core provides some useful core extensions'
|
12
|
+
spec.description = 'Write a longer description. Optional.'
|
13
|
+
spec.homepage = ''
|
14
|
+
spec.license = 'MIT'
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
17
17
|
spec.files.reject! { |fn| fn =~ /^NYSE_closings.pdf/ }
|
18
18
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
19
19
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
20
|
-
spec.require_paths = [
|
20
|
+
spec.require_paths = ['lib']
|
21
21
|
|
22
22
|
spec.add_development_dependency 'simplecov'
|
23
|
-
spec.add_development_dependency
|
24
|
-
spec.add_development_dependency
|
25
|
-
spec.add_development_dependency
|
26
|
-
spec.add_development_dependency
|
27
|
-
spec.add_development_dependency
|
28
|
-
spec.add_development_dependency
|
29
|
-
spec.add_development_dependency
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.5'
|
24
|
+
spec.add_development_dependency 'rake'
|
25
|
+
spec.add_development_dependency 'rspec'
|
26
|
+
spec.add_development_dependency 'byebug'
|
27
|
+
spec.add_development_dependency 'pry'
|
28
|
+
spec.add_development_dependency 'pry-doc'
|
29
|
+
spec.add_development_dependency 'pry-byebug'
|
30
|
+
spec.add_development_dependency 'rcodetools'
|
30
31
|
|
31
|
-
spec.add_runtime_dependency
|
32
|
-
spec.add_runtime_dependency
|
33
|
-
spec.add_runtime_dependency
|
32
|
+
spec.add_runtime_dependency 'activesupport'
|
33
|
+
spec.add_runtime_dependency 'erubis'
|
34
|
+
spec.add_runtime_dependency 'damerau-levenshtein'
|
34
35
|
end
|
data/lib/fat_core.rb
CHANGED
@@ -2,8 +2,9 @@
|
|
2
2
|
require 'date'
|
3
3
|
require 'active_support'
|
4
4
|
require 'active_support/core_ext'
|
5
|
+
require 'csv'
|
5
6
|
|
6
|
-
require
|
7
|
+
require 'fat_core/version'
|
7
8
|
|
8
9
|
require 'fat_core/array'
|
9
10
|
require 'fat_core/date'
|
@@ -17,3 +18,6 @@ require 'fat_core/period'
|
|
17
18
|
require 'fat_core/range'
|
18
19
|
require 'fat_core/string'
|
19
20
|
require 'fat_core/symbol'
|
21
|
+
require 'fat_core/evaluator'
|
22
|
+
require 'fat_core/column'
|
23
|
+
require 'fat_core/table'
|
@@ -0,0 +1,197 @@
|
|
1
|
+
module FatCore
|
2
|
+
# Column objects are just a thin wrapper around an Array to allow columns to
|
3
|
+
# be summed and have other operations performed on them, but compacting out
|
4
|
+
# nils before proceeding. My original attempt to do this by monkey-patching
|
5
|
+
# Array turned out badly. This works much nicer.
|
6
|
+
class Column
|
7
|
+
attr_reader :header, :type, :items
|
8
|
+
|
9
|
+
TYPES = %w(NilClass TrueClass FalseClass Date DateTime Numeric String)
|
10
|
+
|
11
|
+
def initialize(header:, type: 'NilClass', items: [])
|
12
|
+
@header = header.as_sym
|
13
|
+
@type = type
|
14
|
+
raise "Unknown column type '#{type}" unless TYPES.include?(@type.to_s)
|
15
|
+
@items = items
|
16
|
+
end
|
17
|
+
|
18
|
+
def <<(itm)
|
19
|
+
items << convert_to_type(itm)
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](k)
|
23
|
+
items[k]
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_a
|
27
|
+
items
|
28
|
+
end
|
29
|
+
|
30
|
+
def size
|
31
|
+
items.size
|
32
|
+
end
|
33
|
+
|
34
|
+
def last_i
|
35
|
+
size - 1
|
36
|
+
end
|
37
|
+
|
38
|
+
# Return a new Column appending the items of other to our items, checking
|
39
|
+
# for type compatibility.
|
40
|
+
def +(other)
|
41
|
+
raise 'Cannot combine columns with different types' unless type == other.type
|
42
|
+
Column.new(header: header, type: type, items: items + other.items)
|
43
|
+
end
|
44
|
+
|
45
|
+
def first
|
46
|
+
items.compact.first
|
47
|
+
end
|
48
|
+
|
49
|
+
def last
|
50
|
+
items.compact.last
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return a string that of the first and last values.
|
54
|
+
def rng_s
|
55
|
+
"#{first}..#{last}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def sum
|
59
|
+
items.compact.sum
|
60
|
+
end
|
61
|
+
|
62
|
+
def min
|
63
|
+
items.compact.min
|
64
|
+
end
|
65
|
+
|
66
|
+
def max
|
67
|
+
items.compact.max
|
68
|
+
end
|
69
|
+
|
70
|
+
def avg
|
71
|
+
sum / items.compact.size.to_d
|
72
|
+
end
|
73
|
+
|
74
|
+
# Convert val to the type of key, a ruby class constant, such as Date,
|
75
|
+
# Numeric, etc. If type is NilClass, the type is open, and a non-blank val
|
76
|
+
# will attempt conversion to one of the allowed types, typing it as a String
|
77
|
+
# if no other type is recognized. If the val is blank, and the type is nil,
|
78
|
+
# the column type remains open. If the val is nil or a blank and the type is
|
79
|
+
# already determined, the val is set to nil, and should be filtered from any
|
80
|
+
# column computations. If the val is non-blank and the column type
|
81
|
+
# determined, raise an error if the val cannot be converted to the column
|
82
|
+
# type. Otherwise, returns the converted val as an object of the correct
|
83
|
+
# class.
|
84
|
+
def convert_to_type(val)
|
85
|
+
case type
|
86
|
+
when 'NilClass'
|
87
|
+
if val.blank?
|
88
|
+
# Leave the type of the column open
|
89
|
+
val = nil
|
90
|
+
else
|
91
|
+
# Only non-blank values are allowed to set the type of the column
|
92
|
+
val_class = val.class
|
93
|
+
val = convert_to_boolean(val) ||
|
94
|
+
convert_to_date_time(val) ||
|
95
|
+
convert_to_numeric(val) ||
|
96
|
+
convert_to_string(val)
|
97
|
+
@type =
|
98
|
+
if val.is_a?(Numeric)
|
99
|
+
'Numeric'
|
100
|
+
else
|
101
|
+
val.class.name
|
102
|
+
end
|
103
|
+
val
|
104
|
+
end
|
105
|
+
when 'TrueClass', 'FalseClass'
|
106
|
+
val_class = val.class
|
107
|
+
val = convert_to_boolean(val)
|
108
|
+
unless val
|
109
|
+
raise "Inconsistent value in a Boolean column #{key} has class #{val_class}"
|
110
|
+
end
|
111
|
+
val
|
112
|
+
when 'DateTime', 'Date'
|
113
|
+
val_class = val.class
|
114
|
+
val = convert_to_date_time(val)
|
115
|
+
unless val
|
116
|
+
raise "Inconsistent value in a DateTime column #{key} has class #{val_class}"
|
117
|
+
end
|
118
|
+
val
|
119
|
+
when 'Numeric'
|
120
|
+
val_class = val.class
|
121
|
+
val = convert_to_numeric(val)
|
122
|
+
unless val
|
123
|
+
raise "Inconsistent value in a Numeric column #{key} has class #{val_class}"
|
124
|
+
end
|
125
|
+
val
|
126
|
+
when 'String'
|
127
|
+
val_class = val.class
|
128
|
+
val = convert_to_string(val)
|
129
|
+
unless val
|
130
|
+
raise "Inconsistent value in a String column #{key} has class #{val_class}"
|
131
|
+
end
|
132
|
+
val
|
133
|
+
else
|
134
|
+
raise "Unknown object of class #{type} in Table"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Convert the val to a boolean if it looks like one, otherwise return nil.
|
139
|
+
# Any boolean or a string of t, f, true, false, y, n, yes, or no, regardless
|
140
|
+
# of case is assumed to be a boolean.
|
141
|
+
def convert_to_boolean(val)
|
142
|
+
return val if val.is_a?(TrueClass) || val.is_a?(FalseClass)
|
143
|
+
val = val.to_s.clean
|
144
|
+
return nil if val.blank?
|
145
|
+
if val =~ /\Afalse|f|n|no/i
|
146
|
+
false
|
147
|
+
elsif val =~ /\Atrue|t|y|yes\z/i
|
148
|
+
true
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Convert the val to a DateTime if it is either a DateTime, a Date, or a
|
153
|
+
# String that can be parsed as a DateTime, otherwise return nil. It only
|
154
|
+
# recognizes strings that contain a something like '2016-01-14' or
|
155
|
+
# '2/12/1985' within them, otherwise DateTime.parse would treat many bare
|
156
|
+
# numbers as dates, such as '2841381', which it would recognize as a valid
|
157
|
+
# date, but the user probably does not intend it to be so treated.
|
158
|
+
def convert_to_date_time(val)
|
159
|
+
return val if val.is_a?(DateTime)
|
160
|
+
return val.to_datetime if val.is_a?(Date) && type == 'DateTime'
|
161
|
+
return val if val.is_a?(Date)
|
162
|
+
begin
|
163
|
+
val = val.to_s.clean
|
164
|
+
return nil if val.blank?
|
165
|
+
return nil unless val =~ %r{\b\d\d\d\d[-/]\d\d?[-/]\d\d?\b}
|
166
|
+
val = DateTime.parse(val.to_s.clean)
|
167
|
+
val = val.to_date if val.seconds_since_midnight.zero?
|
168
|
+
val
|
169
|
+
rescue ArgumentError
|
170
|
+
return nil
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Convert the val to a Numeric if is already a Numberic or is a String that
|
175
|
+
# looks like one. Any Float is promoted to a BigDecimal. Otherwise return
|
176
|
+
# nil.
|
177
|
+
def convert_to_numeric(val)
|
178
|
+
return BigDecimal.new(val, Float::DIG) if val.is_a?(Float)
|
179
|
+
return val if val.is_a?(Numeric)
|
180
|
+
# Eliminate any commas, $'s, or _'s.
|
181
|
+
val = val.to_s.clean.gsub(/[,_$]/, '')
|
182
|
+
return nil if val.blank?
|
183
|
+
case val
|
184
|
+
when /\A(\d+\.\d*)|(\d*\.\d+)\z/
|
185
|
+
BigDecimal.new(val.to_s.clean)
|
186
|
+
when /\A[\d]+\z/
|
187
|
+
val.to_i
|
188
|
+
when %r{\A(\d+)\s*[:/]\s*(\d+)\z}
|
189
|
+
Rational($1, $2)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def convert_to_string(val)
|
194
|
+
val.to_s
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
data/lib/fat_core/date.rb
CHANGED
@@ -23,45 +23,41 @@ class Date
|
|
23
23
|
# @param str [#to_s] a stringling of the form MM/DD/YYYY
|
24
24
|
# @return [Date] the date represented by the string paramenter.
|
25
25
|
def self.parse_american(str)
|
26
|
-
|
27
|
-
year, month, day = $3.to_i, $1.to_i, $2.to_i
|
28
|
-
if year < 100
|
29
|
-
year += 2000
|
30
|
-
end
|
31
|
-
Date.new(year, month, day)
|
32
|
-
else
|
26
|
+
unless str.to_s =~ %r{\A\s*(\d\d?)\s*/\s*(\d\d?)\s*/\s*(\d?\d?\d\d)\s*\z}
|
33
27
|
raise ArgumentError, "date string must be of form 'MM?/DD?/YY(YY)?'"
|
34
28
|
end
|
29
|
+
year = $3.to_i
|
30
|
+
month = $1.to_i
|
31
|
+
day = $2.to_i
|
32
|
+
year += 2000 if year < 100
|
33
|
+
Date.new(year, month, day)
|
35
34
|
end
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
@
|
62
|
-
respectively, defaulting to interpretation as a to-spec.
|
63
|
-
@return [Date] a date object equivalent to the date spec
|
64
|
-
=end
|
36
|
+
# Convert a 'date spec' to a Date. A date spec is a short-hand way of
|
37
|
+
# specifying a date, relative to the computer clock. A date spec can
|
38
|
+
# interpreted as either a 'from spec' or a 'to spec'.
|
39
|
+
# @example
|
40
|
+
#
|
41
|
+
# Assuming that Date.current at the time of execution is 2014-07-26 and
|
42
|
+
# using the default spec_type of :from. The return values are actually Date
|
43
|
+
# objects, but are shown below as textual dates.
|
44
|
+
#
|
45
|
+
# A fully specified date returns that date:
|
46
|
+
# Date.parse_spec('2001-09-11') # =>
|
47
|
+
# Commercial weeks can be specified using, for example W32 or 32W, with the
|
48
|
+
# week beginning on Monday, ending on Sunday.
|
49
|
+
# Date.parse_spec('2012-W32') # =>
|
50
|
+
# Date.parse_spec('2012-W32', :to) # =>
|
51
|
+
# Date.parse_spec('W32') # =>
|
52
|
+
#
|
53
|
+
# A spec of the form Q3 or 3Q returns the beginning or end of calendar
|
54
|
+
# quarters.
|
55
|
+
# Date.parse_spec('Q3') # =>
|
56
|
+
#
|
57
|
+
# @param spec [#to_s] a stringling containing the spec to be interpreted
|
58
|
+
# @param spec_type [:from, :to] interpret the spec as a from- or to-spec
|
59
|
+
# respectively, defaulting to interpretation as a to-spec.
|
60
|
+
# @return [Date] a date object equivalent to the date spec
|
65
61
|
def self.parse_spec(spec, spec_type = :from)
|
66
62
|
spec = spec.to_s.strip
|
67
63
|
unless [:from, :to].include?(spec_type)
|
@@ -78,16 +74,22 @@ class Date
|
|
78
74
|
if week_num < 1 || week_num > 53
|
79
75
|
raise ArgumentError, "invalid week number (1-53): 'W#{week_num}'"
|
80
76
|
end
|
81
|
-
spec_type == :from
|
77
|
+
if spec_type == :from
|
78
|
+
Date.commercial(today.year, week_num).beginning_of_week
|
79
|
+
else
|
82
80
|
Date.commercial(today.year, week_num).end_of_week
|
81
|
+
end
|
83
82
|
when /\A(\d\d\d\d)-W(\d\d?)\z/, /\A(\d\d\d\d)-(\d\d?)W\z/
|
84
83
|
year = $1.to_i
|
85
84
|
week_num = $2.to_i
|
86
85
|
if week_num < 1 || week_num > 53
|
87
86
|
raise ArgumentError, "invalid week number (1-53): 'W#{week_num}'"
|
88
87
|
end
|
89
|
-
spec_type == :from
|
88
|
+
if spec_type == :from
|
89
|
+
Date.commercial(year, week_num).beginning_of_week
|
90
|
+
else
|
90
91
|
Date.commercial(year, week_num).end_of_week
|
92
|
+
end
|
91
93
|
when /^(\d\d\d\d)-(\d)[Qq]$/, /^(\d\d\d\d)-[Qq](\d)$/
|
92
94
|
# Year-Quarter
|
93
95
|
year = $1.to_i
|
@@ -96,14 +98,21 @@ class Date
|
|
96
98
|
raise ArgumentError, "bad date format: #{spec}"
|
97
99
|
end
|
98
100
|
month = quarter * 3
|
99
|
-
spec_type == :from
|
101
|
+
if spec_type == :from
|
102
|
+
Date.new(year, month, 1).beginning_of_quarter
|
103
|
+
else
|
100
104
|
Date.new(year, month, 1).end_of_quarter
|
105
|
+
end
|
101
106
|
when /^([1234])[qQ]$/, /^[qQ]([1234])$/
|
102
107
|
# Quarter only
|
103
108
|
this_year = today.year
|
104
109
|
quarter = $1.to_i
|
105
110
|
date = Date.new(this_year, quarter * 3, 15)
|
106
|
-
spec_type == :from
|
111
|
+
if spec_type == :from
|
112
|
+
date.beginning_of_quarter
|
113
|
+
else
|
114
|
+
date.end_of_quarter
|
115
|
+
end
|
107
116
|
when /^(\d\d\d\d)-(\d)[Hh]$/, /^(\d\d\d\d)-[Hh](\d)$/
|
108
117
|
# Year-Half
|
109
118
|
year = $1.to_i
|
@@ -112,25 +121,42 @@ class Date
|
|
112
121
|
raise ArgumentError, "bad date format: #{spec}"
|
113
122
|
end
|
114
123
|
month = half * 6
|
115
|
-
spec_type == :from
|
124
|
+
if spec_type == :from
|
125
|
+
Date.new(year, month, 15).beginning_of_half
|
126
|
+
else
|
116
127
|
Date.new(year, month, 1).end_of_half
|
128
|
+
end
|
117
129
|
when /^([12])[hH]$/, /^[hH]([12])$/
|
118
130
|
# Half only
|
119
131
|
this_year = today.year
|
120
132
|
half = $1.to_i
|
121
133
|
date = Date.new(this_year, half * 6, 15)
|
122
|
-
spec_type == :from
|
134
|
+
if spec_type == :from
|
135
|
+
date.beginning_of_half
|
136
|
+
else
|
137
|
+
date.end_of_half
|
138
|
+
end
|
123
139
|
when /^(\d\d\d\d)-(\d\d?)*$/
|
124
140
|
# Year-Month only
|
125
|
-
spec_type == :from
|
141
|
+
if spec_type == :from
|
142
|
+
Date.new($1.to_i, $2.to_i, 1)
|
143
|
+
else
|
126
144
|
Date.new($1.to_i, $2.to_i, 1).end_of_month
|
145
|
+
end
|
127
146
|
when /\A(\d\d?)\z/
|
128
147
|
# Month only
|
129
|
-
spec_type == :from
|
148
|
+
if spec_type == :from
|
149
|
+
Date.new(today.year, $1.to_i, 1)
|
150
|
+
else
|
130
151
|
Date.new(today.year, $1.to_i, 1).end_of_month
|
152
|
+
end
|
131
153
|
when /^(\d\d\d\d)$/
|
132
154
|
# Year only
|
133
|
-
spec_type == :from
|
155
|
+
if spec_type == :from
|
156
|
+
Date.new($1.to_i, 1, 1)
|
157
|
+
else
|
158
|
+
Date.new($1.to_i, 12, 31)
|
159
|
+
end
|
134
160
|
when /^(to|this_?)?day/
|
135
161
|
today
|
136
162
|
when /^(yester|last_?)?day/
|
@@ -138,45 +164,97 @@ class Date
|
|
138
164
|
when /^(this_?)?week/
|
139
165
|
spec_type == :from ? today.beginning_of_week : today.end_of_week
|
140
166
|
when /last_?week/
|
141
|
-
spec_type == :from
|
167
|
+
if spec_type == :from
|
168
|
+
(today - 1.week).beginning_of_week
|
169
|
+
else
|
142
170
|
(today - 1.week).end_of_week
|
171
|
+
end
|
143
172
|
when /^(this_?)?biweek/
|
144
|
-
spec_type == :from
|
173
|
+
if spec_type == :from
|
174
|
+
today.beginning_of_biweek
|
175
|
+
else
|
176
|
+
today.end_of_biweek
|
177
|
+
end
|
145
178
|
when /last_?biweek/
|
146
|
-
spec_type == :from
|
179
|
+
if spec_type == :from
|
180
|
+
(today - 2.week).beginning_of_biweek
|
181
|
+
else
|
147
182
|
(today - 2.week).end_of_biweek
|
183
|
+
end
|
148
184
|
when /^(this_?)?semimonth/
|
149
185
|
spec_type == :from ? today.beginning_of_semimonth : today.end_of_semimonth
|
150
186
|
when /^last_?semimonth/
|
151
|
-
spec_type == :from
|
187
|
+
if spec_type == :from
|
188
|
+
(today - 15.days).beginning_of_semimonth
|
189
|
+
else
|
152
190
|
(today - 15.days).end_of_semimonth
|
191
|
+
end
|
153
192
|
when /^(this_?)?month/
|
154
|
-
spec_type == :from
|
193
|
+
if spec_type == :from
|
194
|
+
today.beginning_of_month
|
195
|
+
else
|
196
|
+
today.end_of_month
|
197
|
+
end
|
155
198
|
when /^last_?month/
|
156
|
-
spec_type == :from
|
199
|
+
if spec_type == :from
|
200
|
+
(today - 1.month).beginning_of_month
|
201
|
+
else
|
157
202
|
(today - 1.month).end_of_month
|
203
|
+
end
|
158
204
|
when /^(this_?)?bimonth/
|
159
|
-
spec_type == :from
|
205
|
+
if spec_type == :from
|
206
|
+
today.beginning_of_bimonth
|
207
|
+
else
|
208
|
+
today.end_of_bimonth
|
209
|
+
end
|
160
210
|
when /^last_?bimonth/
|
161
|
-
spec_type == :from
|
211
|
+
if spec_type == :from
|
212
|
+
(today - 2.month).beginning_of_bimonth
|
213
|
+
else
|
162
214
|
(today - 2.month).end_of_bimonth
|
215
|
+
end
|
163
216
|
when /^(this_?)?quarter/
|
164
|
-
spec_type == :from
|
217
|
+
if spec_type == :from
|
218
|
+
today.beginning_of_quarter
|
219
|
+
else
|
220
|
+
today.end_of_quarter
|
221
|
+
end
|
165
222
|
when /^last_?quarter/
|
166
|
-
spec_type == :from
|
223
|
+
if spec_type == :from
|
224
|
+
(today - 3.months).beginning_of_quarter
|
225
|
+
else
|
167
226
|
(today - 3.months).end_of_quarter
|
227
|
+
end
|
168
228
|
when /^(this_?)?half/
|
169
|
-
spec_type == :from
|
229
|
+
if spec_type == :from
|
230
|
+
today.beginning_of_half
|
231
|
+
else
|
232
|
+
today.end_of_half
|
233
|
+
end
|
170
234
|
when /^last_?half/
|
171
|
-
spec_type == :from
|
235
|
+
if spec_type == :from
|
236
|
+
(today - 6.months).beginning_of_half
|
237
|
+
else
|
172
238
|
(today - 6.months).end_of_half
|
239
|
+
end
|
173
240
|
when /^(this_?)?year/
|
174
|
-
spec_type == :from
|
241
|
+
if spec_type == :from
|
242
|
+
today.beginning_of_year
|
243
|
+
else
|
244
|
+
today.end_of_year
|
245
|
+
end
|
175
246
|
when /^last_?year/
|
176
|
-
spec_type == :from
|
247
|
+
if spec_type == :from
|
248
|
+
(today - 1.year).beginning_of_year
|
249
|
+
else
|
177
250
|
(today - 1.year).end_of_year
|
251
|
+
end
|
178
252
|
when /^forever/
|
179
|
-
spec_type == :from
|
253
|
+
if spec_type == :from
|
254
|
+
Date::BOT
|
255
|
+
else
|
256
|
+
Date::EOT
|
257
|
+
end
|
180
258
|
when /^never/
|
181
259
|
nil
|
182
260
|
else
|
@@ -198,7 +276,7 @@ class Date
|
|
198
276
|
|
199
277
|
# Format as an ISO string.
|
200
278
|
def iso
|
201
|
-
strftime(
|
279
|
+
strftime('%Y-%m-%d')
|
202
280
|
end
|
203
281
|
|
204
282
|
# Format date to TeX documents as ISO strings
|
@@ -208,23 +286,28 @@ class Date
|
|
208
286
|
|
209
287
|
# Format as an all-numeric string, i.e. 'YYYYMMDD'
|
210
288
|
def num
|
211
|
-
strftime(
|
289
|
+
strftime('%Y%m%d')
|
290
|
+
end
|
291
|
+
|
292
|
+
def format_by(fmt = '%Y-%m-%d')
|
293
|
+
fmt ||= '%Y-%m-%d'
|
294
|
+
strftime(fmt)
|
212
295
|
end
|
213
296
|
|
214
297
|
# Format as an inactive Org date (see emacs org-mode)
|
215
298
|
def org
|
216
|
-
strftime(
|
299
|
+
strftime('[%Y-%m-%d %a]')
|
217
300
|
end
|
218
301
|
|
219
302
|
# Format as an English string
|
220
303
|
def eng
|
221
|
-
strftime(
|
304
|
+
strftime('%B %e, %Y')
|
222
305
|
end
|
223
306
|
|
224
307
|
# Format date in MM/DD/YYYY form, as typical for the short American
|
225
308
|
# form.
|
226
309
|
def american
|
227
|
-
strftime
|
310
|
+
strftime '%-m/%-d/%Y'
|
228
311
|
end
|
229
312
|
|
230
313
|
# Does self fall on a weekend?
|
@@ -288,7 +371,7 @@ class Date
|
|
288
371
|
# first day of the odd-numbered months. E.g., 2014-01-01 to
|
289
372
|
# 2014-02-28 is the first bimonth of 2014.
|
290
373
|
def beginning_of_bimonth
|
291
|
-
if month
|
374
|
+
if month.odd?
|
292
375
|
beginning_of_month
|
293
376
|
else
|
294
377
|
(self - 1.month).beginning_of_month
|
@@ -300,7 +383,7 @@ class Date
|
|
300
383
|
# day of the odd-numbered months. E.g., 2014-01-01 to 2014-02-28 is
|
301
384
|
# the first bimonth of 2014.
|
302
385
|
def end_of_bimonth
|
303
|
-
if month
|
386
|
+
if month.odd?
|
304
387
|
(self + 1.month).end_of_month
|
305
388
|
else
|
306
389
|
end_of_month
|
@@ -334,7 +417,7 @@ class Date
|
|
334
417
|
# Note: we use a Monday start of the week in the next two methods because
|
335
418
|
# commercial week counting assumes a Monday start.
|
336
419
|
def beginning_of_biweek
|
337
|
-
if cweek
|
420
|
+
if cweek.odd?
|
338
421
|
beginning_of_week(:monday)
|
339
422
|
else
|
340
423
|
(self - 1.week).beginning_of_week(:monday)
|
@@ -342,7 +425,7 @@ class Date
|
|
342
425
|
end
|
343
426
|
|
344
427
|
def end_of_biweek
|
345
|
-
if cweek
|
428
|
+
if cweek.odd?
|
346
429
|
(self + 1.week).end_of_week(:monday)
|
347
430
|
else
|
348
431
|
end_of_week(:monday)
|
@@ -374,13 +457,11 @@ class Date
|
|
374
457
|
end
|
375
458
|
|
376
459
|
def beginning_of_bimonth?
|
377
|
-
month
|
378
|
-
beginning_of_month == self
|
460
|
+
month.odd? && beginning_of_month == self
|
379
461
|
end
|
380
462
|
|
381
463
|
def end_of_bimonth?
|
382
|
-
month
|
383
|
-
end_of_month == self
|
464
|
+
month.even? && end_of_month == self
|
384
465
|
end
|
385
466
|
|
386
467
|
def beginning_of_month?
|
@@ -500,13 +581,11 @@ class Date
|
|
500
581
|
# executive-order-closing-executive-departments-and-agencies-federal-gover
|
501
582
|
FED_DECREED_HOLIDAYS =
|
502
583
|
[
|
503
|
-
|
504
|
-
]
|
584
|
+
Date.parse('2012-12-24')
|
585
|
+
].freeze
|
505
586
|
|
506
587
|
def self.days_in_month(y, m)
|
507
|
-
if m < 1 || m > 12
|
508
|
-
raise ArgumentError, "illegal month number"
|
509
|
-
end
|
588
|
+
raise ArgumentError, 'illegal month number' if m < 1 || m > 12
|
510
589
|
days = Time::COMMON_YEAR_DAYS_IN_MONTH[m]
|
511
590
|
if m == 2
|
512
591
|
Date.new(y, m, 1).leap? ? 29 : 28
|
@@ -519,20 +598,14 @@ class Date
|
|
519
598
|
# Return the nth weekday in the given month
|
520
599
|
# If n is negative, count from last day of month
|
521
600
|
wday = wday.to_i
|
522
|
-
if wday < 0 || wday > 6
|
523
|
-
raise ArgumentError, "illegal weekday number"
|
524
|
-
end
|
601
|
+
raise ArgumentError, 'illegal weekday number' if wday < 0 || wday > 6
|
525
602
|
month = month.to_i
|
526
|
-
if month < 1 || month > 12
|
527
|
-
raise ArgumentError, "illegal month number"
|
528
|
-
end
|
603
|
+
raise ArgumentError, 'illegal month number' if month < 1 || month > 12
|
529
604
|
n = n.to_i
|
530
605
|
if n > 0
|
531
606
|
# Set d to the 1st wday in month
|
532
607
|
d = Date.new(year, month, 1)
|
533
|
-
while d.wday != wday
|
534
|
-
d += 1
|
535
|
-
end
|
608
|
+
d += 1 while d.wday != wday
|
536
609
|
# Set d to the nth wday in month
|
537
610
|
nd = 1
|
538
611
|
while nd != n
|
@@ -544,9 +617,7 @@ class Date
|
|
544
617
|
n = -n
|
545
618
|
# Set d to the last wday in month
|
546
619
|
d = Date.new(year, month, 1).end_of_month
|
547
|
-
while d.wday != wday
|
548
|
-
d -= 1
|
549
|
-
end
|
620
|
+
d -= 1 while d.wday != wday
|
550
621
|
# Set d to the nth wday in month
|
551
622
|
nd = 1
|
552
623
|
while nd != n
|
@@ -576,11 +647,11 @@ class Date
|
|
576
647
|
g = (b - f + 1) / 3
|
577
648
|
h = (19 * a + b - d - g + 15) % 30
|
578
649
|
i, k = c.divmod(4)
|
579
|
-
l = (32 + 2*e + 2*i - h - k) % 7
|
580
|
-
m = (a + 11*h + 22*l) / 451
|
581
|
-
n, p = (h + l - 7*m + 114).divmod(31)
|
650
|
+
l = (32 + 2 * e + 2 * i - h - k) % 7
|
651
|
+
m = (a + 11 * h + 22 * l) / 451
|
652
|
+
n, p = (h + l - 7 * m + 114).divmod(31)
|
582
653
|
Date.new(y, n, p + 1)
|
583
|
-
|
654
|
+
end
|
584
655
|
|
585
656
|
def easter_this_year
|
586
657
|
# Return the date of Easter in self's year
|
@@ -595,7 +666,7 @@ class Date
|
|
595
666
|
def nth_wday_in_month?(n, wday, month)
|
596
667
|
# Is self the nth weekday in the given month of its year?
|
597
668
|
# If n is negative, count from last day of month
|
598
|
-
self == Date.nth_wday_in_year_month(n, wday,
|
669
|
+
self == Date.nth_wday_in_year_month(n, wday, year, month)
|
599
670
|
end
|
600
671
|
|
601
672
|
#######################################################
|
@@ -626,7 +697,7 @@ class Date
|
|
626
697
|
# rigged to fall on Monday except Thanksgiving
|
627
698
|
|
628
699
|
# No moveable feasts in certain months
|
629
|
-
if [
|
700
|
+
if [3, 4, 6, 7, 8, 12].include?(month)
|
630
701
|
false
|
631
702
|
elsif monday?
|
632
703
|
moveable_mondays = []
|
@@ -658,7 +729,7 @@ class Date
|
|
658
729
|
return true if FED_DECREED_HOLIDAYS.include?(self)
|
659
730
|
|
660
731
|
# Is self a fixed holiday
|
661
|
-
return true if
|
732
|
+
return true if fed_fixed_holiday? || fed_moveable_feast?
|
662
733
|
|
663
734
|
if friday? && month == 12 && day == 26
|
664
735
|
# If Christmas falls on a Thursday, apparently, the Friday after is
|
@@ -717,7 +788,7 @@ class Date
|
|
717
788
|
# rigged to fall on Monday except Thanksgiving
|
718
789
|
|
719
790
|
# No moveable feasts in certain months
|
720
|
-
return false if [
|
791
|
+
return false if [6, 7, 8, 10, 12].include?(month)
|
721
792
|
|
722
793
|
case month
|
723
794
|
when 1
|
@@ -733,13 +804,11 @@ class Date
|
|
733
804
|
# Good Friday
|
734
805
|
if !friday?
|
735
806
|
false
|
736
|
-
|
807
|
+
elsif [1898, 1906, 1907].include?(year)
|
737
808
|
# Good Friday, the Friday before Easter, except certain years
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
(self + 2).easter?
|
742
|
-
end
|
809
|
+
false
|
810
|
+
else
|
811
|
+
(self + 2).easter?
|
743
812
|
end
|
744
813
|
when 5
|
745
814
|
# Memorial Day (Last Monday in May)
|
@@ -760,17 +829,16 @@ class Date
|
|
760
829
|
if year <= 1968
|
761
830
|
is_election_day
|
762
831
|
elsif year <= 1980
|
763
|
-
is_election_day && (year % 4
|
832
|
+
is_election_day && (year % 4).zero?
|
764
833
|
else
|
765
834
|
false
|
766
835
|
end
|
767
836
|
elsif thursday?
|
768
|
-
# Historically Thanksgiving (NYSE closed all day) had been declared to
|
769
|
-
# the last Thursday in November until 1938;
|
770
|
-
#
|
771
|
-
#
|
772
|
-
#
|
773
|
-
# the fourth Thursday in November since 1943;
|
837
|
+
# Historically Thanksgiving (NYSE closed all day) had been declared to
|
838
|
+
# be the last Thursday in November until 1938; the next-to-last
|
839
|
+
# Thursday in November from 1939 to 1941 (therefore the 3rd Thursday
|
840
|
+
# in 1940 and 1941); the last Thursday in November in 1942; the fourth
|
841
|
+
# Thursday in November since 1943;
|
774
842
|
if year < 1938
|
775
843
|
nth_wday_in_month?(-1, 4, 11)
|
776
844
|
elsif year <= 1941
|
@@ -895,19 +963,17 @@ class Date
|
|
895
963
|
def nyse_workday?
|
896
964
|
!nyse_holiday?
|
897
965
|
end
|
898
|
-
alias
|
966
|
+
alias trading_day? nyse_workday?
|
899
967
|
|
900
968
|
def add_fed_business_days(n)
|
901
|
-
d =
|
902
|
-
return d if n
|
969
|
+
d = dup
|
970
|
+
return d if n.zero?
|
903
971
|
incr = n < 0 ? -1 : 1
|
904
972
|
n = n.abs
|
905
973
|
while n > 0
|
906
974
|
d += incr
|
907
|
-
if d.fed_workday?
|
908
|
-
|
909
|
-
end
|
910
|
-
end
|
975
|
+
n -= 1 if d.fed_workday?
|
976
|
+
end
|
911
977
|
d
|
912
978
|
end
|
913
979
|
|
@@ -920,37 +986,33 @@ class Date
|
|
920
986
|
end
|
921
987
|
|
922
988
|
def add_nyse_business_days(n)
|
923
|
-
d =
|
924
|
-
return d if n
|
989
|
+
d = dup
|
990
|
+
return d if n.zero?
|
925
991
|
incr = n < 0 ? -1 : 1
|
926
992
|
n = n.abs
|
927
993
|
while n > 0
|
928
994
|
d += incr
|
929
|
-
if d.nyse_workday?
|
930
|
-
|
931
|
-
end
|
932
|
-
end
|
995
|
+
n -= 1 if d.nyse_workday?
|
996
|
+
end
|
933
997
|
d
|
934
998
|
end
|
935
|
-
alias
|
999
|
+
alias add_trading_days add_nyse_business_days
|
936
1000
|
|
937
1001
|
def next_nyse_workday
|
938
1002
|
add_nyse_business_days(1)
|
939
1003
|
end
|
940
|
-
alias
|
1004
|
+
alias next_trading_day next_nyse_workday
|
941
1005
|
|
942
1006
|
def prior_nyse_workday
|
943
1007
|
add_nyse_business_days(-1)
|
944
1008
|
end
|
945
|
-
alias
|
1009
|
+
alias prior_trading_day prior_nyse_workday
|
946
1010
|
|
947
1011
|
# Return self if its a trading day, otherwise skip back to the first prior
|
948
1012
|
# trading day.
|
949
1013
|
def prior_until_trading_day
|
950
1014
|
date = self
|
951
|
-
|
952
|
-
date -= 1
|
953
|
-
end
|
1015
|
+
date -= 1 until date.trading_day?
|
954
1016
|
date
|
955
1017
|
end
|
956
1018
|
|
@@ -958,9 +1020,7 @@ class Date
|
|
958
1020
|
# later trading day.
|
959
1021
|
def next_until_trading_day
|
960
1022
|
date = self
|
961
|
-
|
962
|
-
date += 1
|
963
|
-
end
|
1023
|
+
date += 1 until date.trading_day?
|
964
1024
|
date
|
965
1025
|
end
|
966
1026
|
end
|