fat_core 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/bin/console +14 -0
- data/fat_core.gemspec +1 -0
- data/lib/fat_core/all.rb +14 -0
- data/lib/fat_core/array.rb +29 -22
- data/lib/fat_core/big_decimal.rb +12 -0
- data/lib/fat_core/date.rb +951 -938
- data/lib/fat_core/enumerable.rb +19 -3
- data/lib/fat_core/hash.rb +48 -43
- data/lib/fat_core/kernel.rb +4 -2
- data/lib/fat_core/nil.rb +13 -13
- data/lib/fat_core/numeric.rb +82 -86
- data/lib/fat_core/range.rb +164 -160
- data/lib/fat_core/string.rb +247 -242
- data/lib/fat_core/symbol.rb +17 -11
- data/lib/fat_core/version.rb +2 -2
- data/lib/fat_core.rb +0 -21
- data/spec/lib/array_spec.rb +2 -0
- data/spec/lib/big_decimal_spec.rb +7 -0
- data/spec/lib/date_spec.rb +7 -26
- data/spec/lib/enumerable_spec.rb +26 -1
- data/spec/lib/hash_spec.rb +2 -0
- data/spec/lib/kernel_spec.rb +2 -0
- data/spec/lib/nil_spec.rb +6 -0
- data/spec/lib/numeric_spec.rb +1 -5
- data/spec/lib/range_spec.rb +1 -1
- data/spec/lib/string_spec.rb +1 -6
- data/spec/lib/symbol_spec.rb +1 -1
- data/spec/spec_helper.rb +0 -2
- metadata +9 -8
- data/lib/fat_core/boolean.rb +0 -25
- data/lib/fat_core/latex_eruby.rb +0 -11
- data/lib/fat_core/period.rb +0 -533
- data/spec/lib/period_spec.rb +0 -677
data/lib/fat_core/period.rb
DELETED
@@ -1,533 +0,0 @@
|
|
1
|
-
# -*- coding: utf-8 -*-
|
2
|
-
|
3
|
-
class Period
|
4
|
-
include Enumerable
|
5
|
-
include Comparable
|
6
|
-
|
7
|
-
attr_reader :first, :last
|
8
|
-
|
9
|
-
def initialize(first, last)
|
10
|
-
case first
|
11
|
-
when String
|
12
|
-
begin
|
13
|
-
first = Date.parse(first)
|
14
|
-
rescue ArgumentError => ex
|
15
|
-
if ex.message =~ /invalid date/
|
16
|
-
raise ArgumentError, "you gave an invalid date '#{first}'"
|
17
|
-
else
|
18
|
-
raise
|
19
|
-
end
|
20
|
-
end
|
21
|
-
when Date
|
22
|
-
first = first
|
23
|
-
else
|
24
|
-
raise ArgumentError, 'use Date or String to initialize Period'
|
25
|
-
end
|
26
|
-
|
27
|
-
case last
|
28
|
-
when String
|
29
|
-
begin
|
30
|
-
last = Date.parse(last)
|
31
|
-
rescue ArgumentError => ex
|
32
|
-
if ex.message =~ /invalid date/
|
33
|
-
raise ArgumentError, "you gave an invalid date '#{last}'"
|
34
|
-
else
|
35
|
-
raise
|
36
|
-
end
|
37
|
-
end
|
38
|
-
when Date
|
39
|
-
last = last
|
40
|
-
else
|
41
|
-
raise ArgumentError, 'use Date or String to initialize Period'
|
42
|
-
end
|
43
|
-
|
44
|
-
@first = first
|
45
|
-
@last = last
|
46
|
-
if @first > @last
|
47
|
-
raise ArgumentError, "Period's first date is later than its last date"
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
# These need to come after initialize is defined
|
52
|
-
TO_DATE = Period.new(Date::BOT, Date.current)
|
53
|
-
FOREVER = Period.new(Date::BOT, Date::EOT)
|
54
|
-
|
55
|
-
# Need custom setters to ensure first <= last
|
56
|
-
def first=(new_first)
|
57
|
-
unless new_first.is_a?(Date)
|
58
|
-
raise ArgumentError, "can't set Period#first to non-date"
|
59
|
-
end
|
60
|
-
unless new_first <= last
|
61
|
-
raise ArgumentError, 'cannot make Period#first > Period#last'
|
62
|
-
end
|
63
|
-
@first = new_first
|
64
|
-
end
|
65
|
-
|
66
|
-
def last=(new_last)
|
67
|
-
unless new_last.is_a?(Date)
|
68
|
-
raise ArgumentError, 'cannot set Period#last to non-date'
|
69
|
-
end
|
70
|
-
unless new_last >= first
|
71
|
-
raise ArgumentError, 'cannot make Period#last < Period#first'
|
72
|
-
end
|
73
|
-
@last = new_last
|
74
|
-
end
|
75
|
-
|
76
|
-
# Comparable base: periods are equal only if their first and last dates are
|
77
|
-
# equal. Sorting will be by first date, then last, so periods starting on
|
78
|
-
# the same date will sort by last date, thus, from smallest to largest in
|
79
|
-
# size.
|
80
|
-
def <=>(other)
|
81
|
-
[first, size] <=> [other.first, other.size]
|
82
|
-
end
|
83
|
-
|
84
|
-
# Comparable does not include this.
|
85
|
-
def !=(other)
|
86
|
-
!(self == other)
|
87
|
-
end
|
88
|
-
|
89
|
-
# Enumerable base. Yield each day in the period.
|
90
|
-
def each
|
91
|
-
d = first
|
92
|
-
while d <= last
|
93
|
-
yield d
|
94
|
-
d += 1.day
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
# Case equality checks for inclusion of date in period.
|
99
|
-
def ===(other)
|
100
|
-
contains?(other)
|
101
|
-
end
|
102
|
-
|
103
|
-
# Return the number of days in the period
|
104
|
-
def days
|
105
|
-
last - first + 1
|
106
|
-
end
|
107
|
-
|
108
|
-
# Return the fractional number of months in the period. By default, use the
|
109
|
-
# average number of days in a month, but allow the user to override the
|
110
|
-
# assumption with a parameter.
|
111
|
-
def months(days_in_month = 30.436875)
|
112
|
-
(days / days_in_month).to_f
|
113
|
-
end
|
114
|
-
|
115
|
-
# Return the fractional number of years in the period. By default, use the
|
116
|
-
# average number of days in a year, but allow the user to override the
|
117
|
-
# assumption with a parameter.
|
118
|
-
def years(days_in_year = 365.2425)
|
119
|
-
(days / days_in_year).to_f
|
120
|
-
end
|
121
|
-
|
122
|
-
def trading_days
|
123
|
-
select(&:nyse_workday?)
|
124
|
-
end
|
125
|
-
|
126
|
-
# Return a period based on two date specs passed as strings (see
|
127
|
-
# Date.parse_spec), a '''from' and a 'to' spec. If the to-spec is not given
|
128
|
-
# or is nil, the from-spec is used for both the from- and to-spec.
|
129
|
-
#
|
130
|
-
# Period.parse('2014-11') => Period.new('2014-11-01', 2014-11-30')
|
131
|
-
# Period.parse('2014-11', '2015-3Q')
|
132
|
-
# => Period.new('2014-11-01', 2015-09-30')
|
133
|
-
def self.parse(from, to = nil)
|
134
|
-
raise ArgumentError, 'Period.parse missing argument' unless from
|
135
|
-
to ||= from
|
136
|
-
first = Date.parse_spec(from, :from)
|
137
|
-
second = Date.parse_spec(to, :to)
|
138
|
-
Period.new(first, second) if first && second
|
139
|
-
end
|
140
|
-
|
141
|
-
# Return a period from a phrase in which the from date is introduced with
|
142
|
-
# 'from' and, optionally, the to-date is introduced with 'to'.
|
143
|
-
#
|
144
|
-
# Period.parse_phrase('from 2014-11 to 2015-3Q')
|
145
|
-
# => Period('2014-11-01', '2015-09-30')
|
146
|
-
def self.parse_phrase(phrase)
|
147
|
-
phrase = phrase.clean
|
148
|
-
if phrase =~ /\Afrom (.*) to (.*)\z/
|
149
|
-
from_phrase = $1
|
150
|
-
to_phrase = $2
|
151
|
-
elsif phrase =~ /\Afrom (.*)\z/
|
152
|
-
from_phrase = $1
|
153
|
-
to_phrase = nil
|
154
|
-
elsif phrase =~ /\Ato (.*)\z/
|
155
|
-
from_phrase = $1
|
156
|
-
else
|
157
|
-
from_phrase = phrase
|
158
|
-
end
|
159
|
-
parse(from_phrase, to_phrase)
|
160
|
-
end
|
161
|
-
|
162
|
-
# Possibly useful class method to take an array of periods and join all the
|
163
|
-
# contiguous ones, then return an array of the disjoint periods not
|
164
|
-
# contiguous to one another. An array of periods with no gaps should return
|
165
|
-
# an array of only one period spanning all the given periods.
|
166
|
-
|
167
|
-
# Return an array of periods that represent the concatenation of all
|
168
|
-
# adjacent periods in the given periods.
|
169
|
-
# def self.meld_periods(*periods)
|
170
|
-
# melded_periods = []
|
171
|
-
# while (this_period = periods.pop)
|
172
|
-
# melded_periods.each do |mp|
|
173
|
-
# if mp.overlaps?(this_period)
|
174
|
-
# melded_periods.delete(mp)
|
175
|
-
# melded_periods << mp.union(this_period)
|
176
|
-
# break
|
177
|
-
# elsif mp.contiguous?(this_period)
|
178
|
-
# melded_periods.delete(mp)
|
179
|
-
# melded_periods << mp.join(this_period)
|
180
|
-
# break
|
181
|
-
# end
|
182
|
-
# end
|
183
|
-
# end
|
184
|
-
# melded_periods
|
185
|
-
# end
|
186
|
-
|
187
|
-
def self.chunk_syms
|
188
|
-
[:day, :week, :biweek, :semimonth, :month, :bimonth,
|
189
|
-
:quarter, :half, :year, :irregular]
|
190
|
-
end
|
191
|
-
|
192
|
-
def self.chunk_sym_to_days(sym)
|
193
|
-
case sym
|
194
|
-
when :day
|
195
|
-
1
|
196
|
-
when :week
|
197
|
-
7
|
198
|
-
when :biweek
|
199
|
-
14
|
200
|
-
when :semimonth
|
201
|
-
15
|
202
|
-
when :month
|
203
|
-
30
|
204
|
-
when :bimonth
|
205
|
-
60
|
206
|
-
when :quarter
|
207
|
-
90
|
208
|
-
when :half
|
209
|
-
180
|
210
|
-
when :year
|
211
|
-
365
|
212
|
-
when :irregular
|
213
|
-
30
|
214
|
-
else
|
215
|
-
raise ArgumentError, "unknown chunk sym '#{sym}'"
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
|
-
# The smallest number of days possible in each chunk
|
220
|
-
def self.chunk_sym_to_min_days(sym)
|
221
|
-
case sym
|
222
|
-
when :semimonth
|
223
|
-
15
|
224
|
-
when :month
|
225
|
-
28
|
226
|
-
when :bimonth
|
227
|
-
59
|
228
|
-
when :quarter
|
229
|
-
86
|
230
|
-
when :half
|
231
|
-
180
|
232
|
-
when :year
|
233
|
-
365
|
234
|
-
when :irregular
|
235
|
-
raise ArgumentError, 'no minimum period for :irregular chunk'
|
236
|
-
else
|
237
|
-
chunk_sym_to_days(sym)
|
238
|
-
end
|
239
|
-
end
|
240
|
-
|
241
|
-
# The largest number of days possible in each chunk
|
242
|
-
def self.chunk_sym_to_max_days(sym)
|
243
|
-
case sym
|
244
|
-
when :semimonth
|
245
|
-
16
|
246
|
-
when :month
|
247
|
-
31
|
248
|
-
when :bimonth
|
249
|
-
62
|
250
|
-
when :quarter
|
251
|
-
92
|
252
|
-
when :half
|
253
|
-
183
|
254
|
-
when :year
|
255
|
-
366
|
256
|
-
when :irregular
|
257
|
-
raise ArgumentError, 'no maximum period for :irregular chunk'
|
258
|
-
else
|
259
|
-
chunk_sym_to_days(sym)
|
260
|
-
end
|
261
|
-
end
|
262
|
-
|
263
|
-
# Distinguishing between :semimonth and :biweek is impossible in
|
264
|
-
# some cases since a :semimonth can be 14 days just like a :biweek.
|
265
|
-
# This ignores that possiblity and requires a :semimonth to be at
|
266
|
-
# least 15 days.
|
267
|
-
def self.days_to_chunk_sym(days)
|
268
|
-
case days
|
269
|
-
when 356..376
|
270
|
-
:year
|
271
|
-
when 180..183
|
272
|
-
:half
|
273
|
-
when 86..96
|
274
|
-
:quarter
|
275
|
-
when 59..62
|
276
|
-
:bimonth
|
277
|
-
when 26..33
|
278
|
-
:month
|
279
|
-
when 15..16
|
280
|
-
:semimonth
|
281
|
-
when 14
|
282
|
-
:biweek
|
283
|
-
when 7
|
284
|
-
:week
|
285
|
-
when 1
|
286
|
-
:day
|
287
|
-
else
|
288
|
-
:irregular
|
289
|
-
end
|
290
|
-
end
|
291
|
-
|
292
|
-
def to_range
|
293
|
-
(first..last)
|
294
|
-
end
|
295
|
-
|
296
|
-
def to_s
|
297
|
-
if first.beginning_of_year? && last.end_of_year? && first.year == last.year
|
298
|
-
first.year.to_s
|
299
|
-
elsif first.beginning_of_quarter? &&
|
300
|
-
last.end_of_quarter? &&
|
301
|
-
first.year == last.year &&
|
302
|
-
first.quarter == last.quarter
|
303
|
-
"#{first.year}-#{first.quarter}Q"
|
304
|
-
elsif first.beginning_of_month? &&
|
305
|
-
last.end_of_month? &&
|
306
|
-
first.year == last.year &&
|
307
|
-
first.month == last.month
|
308
|
-
"#{first.year}-%02d" % first.month
|
309
|
-
else
|
310
|
-
"#{first.iso} to #{last.iso}"
|
311
|
-
end
|
312
|
-
end
|
313
|
-
|
314
|
-
# Allow erb documents can directly interpolate ranges
|
315
|
-
def tex_quote
|
316
|
-
"#{first.iso}--#{last.iso}"
|
317
|
-
end
|
318
|
-
|
319
|
-
# Days in period
|
320
|
-
def size
|
321
|
-
(last - first + 1).to_i
|
322
|
-
end
|
323
|
-
|
324
|
-
def length
|
325
|
-
size
|
326
|
-
end
|
327
|
-
|
328
|
-
def subset_of?(other)
|
329
|
-
to_range.subset_of?(other.to_range)
|
330
|
-
end
|
331
|
-
|
332
|
-
def proper_subset_of?(other)
|
333
|
-
to_range.proper_subset_of?(other.to_range)
|
334
|
-
end
|
335
|
-
|
336
|
-
def superset_of?(other)
|
337
|
-
to_range.superset_of?(other.to_range)
|
338
|
-
end
|
339
|
-
|
340
|
-
def proper_superset_of?(other)
|
341
|
-
to_range.proper_superset_of?(other.to_range)
|
342
|
-
end
|
343
|
-
|
344
|
-
def intersection(other)
|
345
|
-
result = to_range.intersection(other.to_range)
|
346
|
-
if result.nil?
|
347
|
-
nil
|
348
|
-
else
|
349
|
-
Period.new(result.first, result.last)
|
350
|
-
end
|
351
|
-
end
|
352
|
-
alias & intersection
|
353
|
-
alias narrow_to intersection
|
354
|
-
|
355
|
-
def union(other)
|
356
|
-
result = to_range.union(other.to_range)
|
357
|
-
Period.new(result.first, result.last)
|
358
|
-
end
|
359
|
-
alias + union
|
360
|
-
|
361
|
-
def difference(other)
|
362
|
-
ranges = to_range.difference(other.to_range)
|
363
|
-
ranges.each.map { |r| Period.new(r.first, r.last) }
|
364
|
-
end
|
365
|
-
alias - difference
|
366
|
-
|
367
|
-
# returns the chunk sym represented by the period
|
368
|
-
def chunk_sym
|
369
|
-
if first.beginning_of_year? && last.end_of_year? &&
|
370
|
-
(365..366) === last - first + 1
|
371
|
-
:year
|
372
|
-
elsif first.beginning_of_half? && last.end_of_half? &&
|
373
|
-
(180..183) === last - first + 1
|
374
|
-
:half
|
375
|
-
elsif first.beginning_of_quarter? && last.end_of_quarter? &&
|
376
|
-
(90..92) === last - first + 1
|
377
|
-
:quarter
|
378
|
-
elsif first.beginning_of_bimonth? && last.end_of_bimonth? &&
|
379
|
-
(58..62) === last - first + 1
|
380
|
-
:bimonth
|
381
|
-
elsif first.beginning_of_month? && last.end_of_month? &&
|
382
|
-
(28..31) === last - first + 1
|
383
|
-
:month
|
384
|
-
elsif first.beginning_of_semimonth? && last.end_of_semimonth &&
|
385
|
-
(13..16) === last - first + 1
|
386
|
-
:semimonth
|
387
|
-
elsif first.beginning_of_biweek? && last.end_of_biweek? &&
|
388
|
-
last - first + 1 == 14
|
389
|
-
:biweek
|
390
|
-
elsif first.beginning_of_week? && last.end_of_week? &&
|
391
|
-
last - first + 1 == 7
|
392
|
-
:week
|
393
|
-
elsif first == last
|
394
|
-
:day
|
395
|
-
else
|
396
|
-
:irregular
|
397
|
-
end
|
398
|
-
end
|
399
|
-
|
400
|
-
# Name for a period not necessarily ending on calendar boundaries. For
|
401
|
-
# example, in reporting reconciliation, we want the period from Feb 11,
|
402
|
-
# 2014, to March 10, 2014, be called the 'Month ending March 10, 2014,'
|
403
|
-
# event though the period is not a calendar month. Using the stricter
|
404
|
-
# Period#chunk_sym, would not allow such looseness.
|
405
|
-
def chunk_name
|
406
|
-
case Period.days_to_chunk_sym(length)
|
407
|
-
when :year
|
408
|
-
'Year'
|
409
|
-
when :half
|
410
|
-
'Half'
|
411
|
-
when :quarter
|
412
|
-
'Quarter'
|
413
|
-
when :bimonth
|
414
|
-
'Bi-month'
|
415
|
-
when :month
|
416
|
-
'Month'
|
417
|
-
when :semimonth
|
418
|
-
'Semi-month'
|
419
|
-
when :biweek
|
420
|
-
'Bi-week'
|
421
|
-
when :week
|
422
|
-
'Week'
|
423
|
-
when :day
|
424
|
-
'Day'
|
425
|
-
else
|
426
|
-
'Period'
|
427
|
-
end
|
428
|
-
end
|
429
|
-
|
430
|
-
def contains?(date)
|
431
|
-
date = date.to_date if date.respond_to?(:to_date)
|
432
|
-
raise ArgumentError, 'argument must be a Date' unless date.is_a?(Date)
|
433
|
-
to_range.cover?(date)
|
434
|
-
end
|
435
|
-
|
436
|
-
def overlaps?(other)
|
437
|
-
to_range.overlaps?(other.to_range)
|
438
|
-
end
|
439
|
-
|
440
|
-
# Return whether any of the Periods that are within self overlap one
|
441
|
-
# another
|
442
|
-
def has_overlaps_within?(periods)
|
443
|
-
to_range.has_overlaps_within?(periods.map(&:to_range))
|
444
|
-
end
|
445
|
-
|
446
|
-
def spanned_by?(periods)
|
447
|
-
to_range.spanned_by?(periods.map(&:to_range))
|
448
|
-
end
|
449
|
-
|
450
|
-
def gaps(periods)
|
451
|
-
to_range.gaps(periods.map(&:to_range))
|
452
|
-
.map { |r| Period.new(r.first, r.last) }
|
453
|
-
end
|
454
|
-
|
455
|
-
# Return an array of Periods wholly-contained within self in chunks of size,
|
456
|
-
# defaulting to monthly chunks. Partial chunks at the beginning and end of
|
457
|
-
# self are not included unless partial_first or partial_last, respectively,
|
458
|
-
# are set true. The last chunk can be made to extend beyond the end of self to
|
459
|
-
# make it a whole chunk if round_up_last is set true, in which case,
|
460
|
-
# partial_last is ignored.
|
461
|
-
def chunks(size: :month, partial_first: false, partial_last: false,
|
462
|
-
round_up_last: false)
|
463
|
-
size = size.to_sym
|
464
|
-
if Period.chunk_sym_to_min_days(size) > length
|
465
|
-
if partial_first || partial_last
|
466
|
-
return [self]
|
467
|
-
else
|
468
|
-
raise ArgumentError, "any #{size} is longer than this period's #{length} days"
|
469
|
-
end
|
470
|
-
end
|
471
|
-
result = []
|
472
|
-
chunk_start = first.dup
|
473
|
-
while chunk_start <= last
|
474
|
-
case size
|
475
|
-
when :year
|
476
|
-
unless partial_first
|
477
|
-
chunk_start += 1.day until chunk_start.beginning_of_year?
|
478
|
-
end
|
479
|
-
chunk_end = chunk_start.end_of_year
|
480
|
-
when :half
|
481
|
-
unless partial_first
|
482
|
-
chunk_start += 1.day until chunk_start.beginning_of_half?
|
483
|
-
end
|
484
|
-
chunk_end = chunk_start.end_of_half
|
485
|
-
when :quarter
|
486
|
-
unless partial_first
|
487
|
-
chunk_start += 1.day until chunk_start.beginning_of_quarter?
|
488
|
-
end
|
489
|
-
chunk_end = chunk_start.end_of_quarter
|
490
|
-
when :bimonth
|
491
|
-
unless partial_first
|
492
|
-
chunk_start += 1.day until chunk_start.beginning_of_bimonth?
|
493
|
-
end
|
494
|
-
chunk_end = (chunk_start.end_of_month + 1.day).end_of_month
|
495
|
-
when :month
|
496
|
-
unless partial_first
|
497
|
-
chunk_start += 1.day until chunk_start.beginning_of_month?
|
498
|
-
end
|
499
|
-
chunk_end = chunk_start.end_of_month
|
500
|
-
when :semimonth
|
501
|
-
unless partial_first
|
502
|
-
chunk_start += 1.day until chunk_start.beginning_of_semimonth?
|
503
|
-
end
|
504
|
-
chunk_end = chunk_start.end_of_semimonth
|
505
|
-
when :biweek
|
506
|
-
unless partial_first
|
507
|
-
chunk_start += 1.day until chunk_start.beginning_of_biweek?
|
508
|
-
end
|
509
|
-
chunk_end = chunk_start.end_of_biweek
|
510
|
-
when :week
|
511
|
-
unless partial_first
|
512
|
-
chunk_start += 1.day until chunk_start.beginning_of_week?
|
513
|
-
end
|
514
|
-
chunk_end = chunk_start.end_of_week
|
515
|
-
when :day
|
516
|
-
chunk_end = chunk_start
|
517
|
-
else
|
518
|
-
raise ArgumentError, "invalid chunk size '#{size}'"
|
519
|
-
end
|
520
|
-
if chunk_end <= last
|
521
|
-
result << Period.new(chunk_start, chunk_end)
|
522
|
-
elsif round_up_last
|
523
|
-
result << Period.new(chunk_start, chunk_end)
|
524
|
-
elsif partial_last
|
525
|
-
result << Period.new(chunk_start, last)
|
526
|
-
else
|
527
|
-
break
|
528
|
-
end
|
529
|
-
chunk_start = result.last.last + 1.day
|
530
|
-
end
|
531
|
-
result
|
532
|
-
end
|
533
|
-
end
|