fat_core 2.0.1 → 3.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/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
|