fat_core 1.0.3 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|