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


147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/fat_core/period.rb', line 147

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 :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


173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/fat_core/period.rb', line 173

def self.chunk_sym_to_max_days(sym)
  case sym
  when :semimonth
    16
  when :month
    31
  when :bimonth
    62
  when :quarter
    92
  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


142
143
144
145
# File 'lib/fat_core/period.rb', line 142

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

.days_to_chunk_sym(days) ⇒ Object

This is only used for inferring statement frequency based on the number of days between statements, so it will not consider all possible chunks, only :year, :quarter, :month, and :week. And 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.


199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/fat_core/period.rb', line 199

def self.days_to_chunk_sym(days)
  case days
  when 356..376
    :year
  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_spec(from = 'today', to = nil) ⇒ Object

Return a period based on two date specs (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. If no from-spec is given, return today as the period.


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

def self.parse_spec(from = 'today', to = nil)
  to ||= from
  from ||= to
  Period.new(Date.parse_spec(from, :from), Date.parse_spec(to, :to))
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

#===(date) ⇒ Object

Case equality checks for inclusion of date in period.


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

def ===(date)
  self.contains?(date)
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.


336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
# File 'lib/fat_core/period.rb', line 336

def chunk_name
  case Period.days_to_chunk_sym(length)
  when :year
    'Year'
  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


302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/fat_core/period.rb', line 302

def chunk_sym
  if first.beginning_of_year? && last.end_of_year? &&
      (365..366) === last - first + 1
    :year
  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.


394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
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
# File 'lib/fat_core/period.rb', line 394

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
        until chunk_start.beginning_of_year?
          chunk_start += 1.day
        end
      end
      chunk_end = chunk_start.end_of_year
    when :quarter
      unless partial_first
        until chunk_start.beginning_of_quarter?
          chunk_start += 1.day
        end
      end
      chunk_end = chunk_start.end_of_quarter
    when :bimonth
      unless partial_first
        until chunk_start.beginning_of_bimonth?
          chunk_start += 1.day
        end
      end
      chunk_end = (chunk_start.end_of_month + 1.day).end_of_month
    when :month
      unless partial_first
        until chunk_start.beginning_of_month?
          chunk_start += 1.day
        end
      end
      chunk_end = chunk_start.end_of_month
    when :semimonth
      unless partial_first
        until chunk_start.beginning_of_semimonth?
          chunk_start += 1.day
        end
      end
      chunk_end = chunk_start.end_of_semimonth
    when :biweek
      unless partial_first
        until chunk_start.beginning_of_biweek?
          chunk_start += 1.day
        end
      end
      chunk_end = chunk_start.end_of_biweek
    when :week
      unless partial_first
        until chunk_start.beginning_of_week?
          chunk_start += 1.day
        end
      end
      chunk_end = chunk_start.end_of_week
    when :day
      chunk_end = chunk_start
    else
      chunk_end = last
    end
    if chunk_end <= last
      result << Period.new(chunk_start, chunk_end)
    else
      if round_up_last
        result << Period.new(chunk_start, chunk_end)
      elsif partial_last
        result << Period.new(chunk_start, last)
      else
        break
      end
    end
    chunk_start = result.last.last + 1.day
  end
  result
end

#contains?(date) ⇒ Boolean

Returns:

  • (Boolean)

359
360
361
362
363
364
365
366
367
# File 'lib/fat_core/period.rb', line 359

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

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


295
296
297
298
# File 'lib/fat_core/period.rb', line 295

def difference(other)
  ranges = self.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 = d + 1.day
  end
end

#gaps(periods) ⇒ Object


383
384
385
386
# File 'lib/fat_core/period.rb', line 383

def gaps(periods)
  to_range.gaps(periods.map { |p| p.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)

375
376
377
# File 'lib/fat_core/period.rb', line 375

def has_overlaps_within?(periods)
  self.to_range.has_overlaps_within?(periods.map{ |p| p.to_range})
end

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


278
279
280
281
282
283
284
285
# File 'lib/fat_core/period.rb', line 278

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

#lengthObject


254
255
256
# File 'lib/fat_core/period.rb', line 254

def length
  size
end

#overlaps?(other) ⇒ Boolean

Returns:

  • (Boolean)

274
275
276
# File 'lib/fat_core/period.rb', line 274

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

#proper_subset_of?(other) ⇒ Boolean

Returns:

  • (Boolean)

262
263
264
# File 'lib/fat_core/period.rb', line 262

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

#proper_superset_of?(other) ⇒ Boolean

Returns:

  • (Boolean)

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

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

#sizeObject

Days in period


250
251
252
# File 'lib/fat_core/period.rb', line 250

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

#spanned_by?(periods) ⇒ Boolean

Returns:

  • (Boolean)

379
380
381
# File 'lib/fat_core/period.rb', line 379

def spanned_by?(periods)
  to_range.spanned_by?(periods.map { |p| p.to_range })
end

#subset_of?(other) ⇒ Boolean

Returns:

  • (Boolean)

258
259
260
# File 'lib/fat_core/period.rb', line 258

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

#superset_of?(other) ⇒ Boolean

Returns:

  • (Boolean)

266
267
268
# File 'lib/fat_core/period.rb', line 266

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

#tex_quoteObject

Allow erb documents can directly interpolate ranges


245
246
247
# File 'lib/fat_core/period.rb', line 245

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

#to_rangeObject


222
223
224
# File 'lib/fat_core/period.rb', line 222

def to_range
  (first..last)
end

#to_sObject


226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/fat_core/period.rb', line 226

def to_s
  if first.beginning_of_year? && last.end_of_year? && first.year == last.year
    "#{first.year}"
  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


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

def trading_days
  select(&:nyse_workday?)
end

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


289
290
291
292
# File 'lib/fat_core/period.rb', line 289

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