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.
@@ -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