Class: Period

Inherits:
Object
  • Object
show all
Includes:
Comparable, Enumerable
Defined in:
lib/fat_core/period.rb

Constant Summary collapse

TO_DATE =

These need to come after initialize is defined

Period.new(Date::BOT, Date.current)
FOREVER =
Period.new(Date::BOT, Date::EOT)

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Enumerable

#groups_of

Constructor Details

#initialize(first, last) ⇒ Period

Returns a new instance of Period.


9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/fat_core/period.rb', line 9

def initialize(first, last)
  case first
  when String
    begin
      first = Date.parse(first)
    rescue ArgumentError => ex
      if ex.message =~ /invalid date/
        raise ArgumentError, "you gave an invalid date '#{first}'"
      else
        raise
      end
    end
  when Date
    first = first
  else
    raise ArgumentError, 'use Date or String to initialize Period'
  end

  case last
  when String
    begin
      last = Date.parse(last)
    rescue ArgumentError => ex
      if ex.message =~ /invalid date/
        raise ArgumentError, "you gave an invalid date '#{last}'"
      else
        raise
      end
    end
  when Date
    last = last
  else
    raise ArgumentError, 'use Date or String to initialize Period'
  end

  @first = first
  @last = last
  if @first > @last
    raise ArgumentError, "Period's first date is later than its last date"
  end
end

Instance Attribute Details

#firstObject

Returns the value of attribute first.


7
8
9
# File 'lib/fat_core/period.rb', line 7

def first
  @first
end

#lastObject

Returns the value of attribute last.


7
8
9
# File 'lib/fat_core/period.rb', line 7

def last
  @last
end

Class Method Details

.chunk_sym_to_days(sym) ⇒ Object


192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/fat_core/period.rb', line 192

def self.chunk_sym_to_days(sym)
  case sym
  when :day
    1
  when :week
    7
  when :biweek
    14
  when :semimonth
    15
  when :month
    30
  when :bimonth
    60
  when :quarter
    90
  when :half
    180
  when :year
    365
  when :irregular
    30
  else
    raise ArgumentError, "unknown chunk sym '#{sym}'"
  end
end

.chunk_sym_to_max_days(sym) ⇒ Object

The largest number of days possible in each chunk


220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/fat_core/period.rb', line 220

def self.chunk_sym_to_max_days(sym)
  case sym
  when :semimonth
    16
  when :month
    31
  when :bimonth
    62
  when :quarter
    92
  when :half
    183
  when :year
    366
  when :irregular
    raise ArgumentError, 'no maximum period for :irregular chunk'
  else
    chunk_sym_to_days(sym)
  end
end

.chunk_symsObject

Return an array of periods that represent the concatenation of all adjacent periods in the given periods. def self.meld_periods(*periods)

melded_periods = []
while (this_period = periods.pop)
  melded_periods.each do |mp|
    if mp.overlaps?(this_period)
      melded_periods.delete(mp)
      melded_periods << mp.union(this_period)
      break
    elsif mp.contiguous?(this_period)
      melded_periods.delete(mp)
      melded_periods << mp.join(this_period)
      break
    end
  end
end
melded_periods

end


187
188
189
190
# File 'lib/fat_core/period.rb', line 187

def self.chunk_syms
  [:day, :week, :biweek, :semimonth, :month, :bimonth,
   :quarter, :half, :year, :irregular]
end

.days_to_chunk_sym(days) ⇒ Object

Distinguishing between :semimonth and :biweek is impossible in some cases since a :semimonth can be 14 days just like a :biweek. This ignores that possiblity and requires a :semimonth to be at least 15 days.


245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/fat_core/period.rb', line 245

def self.days_to_chunk_sym(days)
  case days
  when 356..376
    :year
  when 180..183
    :half
  when 86..96
    :quarter
  when 59..62
    :bimonth
  when 26..33
    :month
  when 15..16
    :semimonth
  when 14
    :biweek
  when 7
    :week
  when 1
    :day
  else
    :irregular
  end
end

.parse(from, to = nil) ⇒ Object

Return a period based on two date specs passed as strings (see Date.parse_spec), a ”‘from’ and a ‘to’ spec. If the to-spec is not given or is nil, the from-spec is used for both the from- and to-spec.

Period.parse(‘2014-11’) => Period.new(‘2014-11-01’, 2014-11-30’) Period.parse(‘2014-11’, ‘2015-3Q’)

=> Period.new('2014-11-01', 2015-09-30')

Raises:

  • (ArgumentError)

133
134
135
136
137
138
139
# File 'lib/fat_core/period.rb', line 133

def self.parse(from, to = nil)
  raise ArgumentError, 'Period.parse missing argument' unless from
  to ||= from
  first = Date.parse_spec(from, :from)
  second = Date.parse_spec(to, :to)
  Period.new(first, second) if first && second
end

.parse_phrase(phrase) ⇒ Object

Return a period from a phrase in which the from date is introduced with ‘from’ and, optionally, the to-date is introduced with ‘to’.

Period.parse_phrase(‘from 2014-11 to 2015-3Q’)

=> Period('2014-11-01', '2015-09-30')

146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/fat_core/period.rb', line 146

def self.parse_phrase(phrase)
  phrase = phrase.clean
  if phrase =~ /\Afrom (.*) to (.*)\z/
    from_phrase = $1
    to_phrase = $2
  elsif phrase =~ /\Afrom (.*)\z/
    from_phrase = $1
    to_phrase = nil
  elsif phrase =~ /\Ato (.*)\z/
    from_phrase = $1
  else
    from_phrase = phrase
  end
  parse(from_phrase, to_phrase)
end

Instance Method Details

#!=(other) ⇒ Object

Comparable does not include this.


85
86
87
# File 'lib/fat_core/period.rb', line 85

def !=(other)
  !(self == other)
end

#<=>(other) ⇒ Object

Comparable base: periods are equal only if their first and last dates are equal. Sorting will be by first date, then last, so periods starting on the same date will sort by last date, thus, from smallest to largest in size.


80
81
82
# File 'lib/fat_core/period.rb', line 80

def <=>(other)
  [first, size] <=> [other.first, other.size]
end

#===(other) ⇒ Object

Case equality checks for inclusion of date in period.


99
100
101
# File 'lib/fat_core/period.rb', line 99

def ===(other)
  contains?(other)
end

#chunk_nameObject

Name for a period not necessarily ending on calendar boundaries. For example, in reporting reconciliation, we want the period from Feb 11, 2014, to March 10, 2014, be called the ‘Month ending March 10, 2014,’ event though the period is not a calendar month. Using the stricter Period#chunk_sym, would not allow such looseness.


383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/fat_core/period.rb', line 383

def chunk_name
  case Period.days_to_chunk_sym(length)
  when :year
    'Year'
  when :half
    'Half'
  when :quarter
    'Quarter'
  when :bimonth
    'Bi-month'
  when :month
    'Month'
  when :semimonth
    'Semi-month'
  when :biweek
    'Bi-week'
  when :week
    'Week'
  when :day
    'Day'
  else
    'Period'
  end
end

#chunk_symObject

returns the chunk sym represented by the period


346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/fat_core/period.rb', line 346

def chunk_sym
  if first.beginning_of_year? && last.end_of_year? &&
     (365..366) === last - first + 1
    :year
  elsif first.beginning_of_half? && last.end_of_half? &&
        (180..183) === last - first + 1
    :half
  elsif first.beginning_of_quarter? && last.end_of_quarter? &&
        (90..92) === last - first + 1
    :quarter
  elsif first.beginning_of_bimonth? && last.end_of_bimonth? &&
        (58..62) === last - first + 1
    :bimonth
  elsif first.beginning_of_month? && last.end_of_month? &&
        (28..31) === last - first + 1
    :month
  elsif first.beginning_of_semimonth? && last.end_of_semimonth &&
        (13..16) === last - first + 1
    :semimonth
  elsif first.beginning_of_biweek? && last.end_of_biweek? &&
        last - first + 1 == 14
    :biweek
  elsif first.beginning_of_week? && last.end_of_week? &&
        last - first + 1 == 7
    :week
  elsif first == last
    :day
  else
    :irregular
  end
end

#chunks(size: :month, partial_first: false, partial_last: false, round_up_last: false) ⇒ Object

Return an array of Periods wholly-contained within self in chunks of size, defaulting to monthly chunks. Partial chunks at the beginning and end of self are not included unless partial_first or partial_last, respectively, are set true. The last chunk can be made to extend beyond the end of self to make it a whole chunk if round_up_last is set true, in which case, partial_last is ignored.


439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# File 'lib/fat_core/period.rb', line 439

def chunks(size: :month, partial_first: false, partial_last: false,
           round_up_last: false)
  size = size.to_sym
  result = []
  chunk_start = first.dup
  while chunk_start <= last
    case size
    when :year
      unless partial_first
        chunk_start += 1.day until chunk_start.beginning_of_year?
      end
      chunk_end = chunk_start.end_of_year
    when :half
      unless partial_first
        chunk_start += 1.day until chunk_start.beginning_of_half?
      end
      chunk_end = chunk_start.end_of_half
    when :quarter
      unless partial_first
        chunk_start += 1.day until chunk_start.beginning_of_quarter?
      end
      chunk_end = chunk_start.end_of_quarter
    when :bimonth
      unless partial_first
        chunk_start += 1.day until chunk_start.beginning_of_bimonth?
      end
      chunk_end = (chunk_start.end_of_month + 1.day).end_of_month
    when :month
      unless partial_first
        chunk_start += 1.day until chunk_start.beginning_of_month?
      end
      chunk_end = chunk_start.end_of_month
    when :semimonth
      unless partial_first
        chunk_start += 1.day until chunk_start.beginning_of_semimonth?
      end
      chunk_end = chunk_start.end_of_semimonth
    when :biweek
      unless partial_first
        chunk_start += 1.day until chunk_start.beginning_of_biweek?
      end
      chunk_end = chunk_start.end_of_biweek
    when :week
      unless partial_first
        chunk_start += 1.day until chunk_start.beginning_of_week?
      end
      chunk_end = chunk_start.end_of_week
    when :day
      chunk_end = chunk_start
    else
      raise ArgumentError, "invalid chunk size '#{size}'"
    end
    if chunk_end <= last
      result << Period.new(chunk_start, chunk_end)
    elsif round_up_last
      result << Period.new(chunk_start, chunk_end)
    elsif partial_last
      result << Period.new(chunk_start, last)
    else
      break
    end
    chunk_start = result.last.last + 1.day
  end
  result
end

#contains?(date) ⇒ Boolean

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)

408
409
410
411
412
# File 'lib/fat_core/period.rb', line 408

def contains?(date)
  date = date.to_date if date.respond_to?(:to_date)
  raise ArgumentError, 'argument must be a Date' unless date.is_a?(Date)
  to_range.cover?(date)
end

#daysObject

Return the number of days in the period


104
105
106
# File 'lib/fat_core/period.rb', line 104

def days
  last - first + 1
end

#difference(other) ⇒ Object Also known as: -


339
340
341
342
# File 'lib/fat_core/period.rb', line 339

def difference(other)
  ranges = to_range.difference(other.to_range)
  ranges.each.map { |r| Period.new(r.first, r.last) }
end

#eachObject

Enumerable base. Yield each day in the period.


90
91
92
93
94
95
96
# File 'lib/fat_core/period.rb', line 90

def each
  d = first
  while d <= last
    yield d
    d += 1.day
  end
end

#gaps(periods) ⇒ Object


428
429
430
431
# File 'lib/fat_core/period.rb', line 428

def gaps(periods)
  to_range.gaps(periods.map(&:to_range))
    .map { |r| Period.new(r.first, r.last) }
end

#has_overlaps_within?(periods) ⇒ Boolean

Return whether any of the Periods that are within self overlap one another

Returns:

  • (Boolean)

420
421
422
# File 'lib/fat_core/period.rb', line 420

def has_overlaps_within?(periods)
  to_range.has_overlaps_within?(periods.map(&:to_range))
end

#intersection(other) ⇒ Object Also known as: &, narrow_to


322
323
324
325
326
327
328
329
# File 'lib/fat_core/period.rb', line 322

def intersection(other)
  result = to_range.intersection(other.to_range)
  if result.nil?
    nil
  else
    Period.new(result.first, result.last)
  end
end

#lengthObject


302
303
304
# File 'lib/fat_core/period.rb', line 302

def length
  size
end

#months(days_in_month = 30.436875) ⇒ Object

Return the fractional number of months in the period. By default, use the average number of days in a month, but allow the user to override the assumption with a parameter.


111
112
113
# File 'lib/fat_core/period.rb', line 111

def months(days_in_month = 30.436875)
  (days / days_in_month).to_f
end

#overlaps?(other) ⇒ Boolean

Returns:

  • (Boolean)

414
415
416
# File 'lib/fat_core/period.rb', line 414

def overlaps?(other)
  to_range.overlaps?(other.to_range)
end

#proper_subset_of?(other) ⇒ Boolean

Returns:

  • (Boolean)

310
311
312
# File 'lib/fat_core/period.rb', line 310

def proper_subset_of?(other)
  to_range.proper_subset_of?(other.to_range)
end

#proper_superset_of?(other) ⇒ Boolean

Returns:

  • (Boolean)

318
319
320
# File 'lib/fat_core/period.rb', line 318

def proper_superset_of?(other)
  to_range.proper_superset_of?(other.to_range)
end

#sizeObject

Days in period


298
299
300
# File 'lib/fat_core/period.rb', line 298

def size
  (last - first + 1).to_i
end

#spanned_by?(periods) ⇒ Boolean

Returns:

  • (Boolean)

424
425
426
# File 'lib/fat_core/period.rb', line 424

def spanned_by?(periods)
  to_range.spanned_by?(periods.map(&:to_range))
end

#subset_of?(other) ⇒ Boolean

Returns:

  • (Boolean)

306
307
308
# File 'lib/fat_core/period.rb', line 306

def subset_of?(other)
  to_range.subset_of?(other.to_range)
end

#superset_of?(other) ⇒ Boolean

Returns:

  • (Boolean)

314
315
316
# File 'lib/fat_core/period.rb', line 314

def superset_of?(other)
  to_range.superset_of?(other.to_range)
end

#tex_quoteObject

Allow erb documents can directly interpolate ranges


293
294
295
# File 'lib/fat_core/period.rb', line 293

def tex_quote
  "#{first.iso}--#{last.iso}"
end

#to_rangeObject


270
271
272
# File 'lib/fat_core/period.rb', line 270

def to_range
  (first..last)
end

#to_sObject


274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
# File 'lib/fat_core/period.rb', line 274

def to_s
  if first.beginning_of_year? && last.end_of_year? && first.year == last.year
    first.year.to_s
  elsif first.beginning_of_quarter? &&
        last.end_of_quarter? &&
        first.year == last.year &&
        first.quarter == last.quarter
    "#{first.year}-#{first.quarter}Q"
  elsif first.beginning_of_month? &&
        last.end_of_month? &&
        first.year == last.year &&
        first.month == last.month
    "#{first.year}-%02d" % first.month
  else
    "#{first.iso} to #{last.iso}"
  end
end

#trading_daysObject


122
123
124
# File 'lib/fat_core/period.rb', line 122

def trading_days
  select(&:nyse_workday?)
end

#union(other) ⇒ Object Also known as: +


333
334
335
336
# File 'lib/fat_core/period.rb', line 333

def union(other)
  result = to_range.union(other.to_range)
  Period.new(result.first, result.last)
end

#years(days_in_year = 365.2425) ⇒ Object

Return the fractional number of years in the period. By default, use the average number of days in a year, but allow the user to override the assumption with a parameter.


118
119
120
# File 'lib/fat_core/period.rb', line 118

def years(days_in_year = 365.2425)
  (days / days_in_year).to_f
end