Class: Timeframe

Inherits:
Object
  • Object
show all
Defined in:
lib/timeframe.rb,
lib/timeframe/version.rb,
lib/timeframe/iso_8601.rb

Overview

Encapsulates a timeframe between two dates. The dates provided to the class are always until the last date. That means that the last date is excluded.

# from 2007-10-01 00:00:00.000 to 2007-10-31 23:59:59.999
Timeframe.new(Date(2007,10,1), Date(2007,11,1))
# and holds 31 days
Timeframe.new(Date(2007,10,1), Date(2007,11,1)).days #=> 31

Defined Under Namespace

Modules: Iso8601

Constant Summary collapse

VERSION =
'1.0.0'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ Timeframe

Creates a new instance of Timeframe. You can either pass a start and end Date or a Hash with named arguments, with the following options:

<tt>:month</tt>: Start date becomes the first day of this month, and the end date becomes the first day of
the next month. If no <tt>:year</tt> is specified, the current year is used.
<tt>:year</tt>: Start date becomes the first day of this year, and the end date becomes the first day of the
next year.

Examples:

Timeframe.new Date.new(2007, 2, 1), Date.new(2007, 4, 1) # February and March
Timeframe.new :year => 2004 # The year 2004
Timeframe.new :month => 4 # April
Timeframe.new :year => 2004, :month => 2 # Feburary 2004

Raises:

  • (ArgumentError)


134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/timeframe.rb', line 134

def initialize(*args)
  options = args.extract_options!

  if month = options[:month]
    month = Date.parse(month).month if month.is_a? String
    year = options[:year] || Date.today.year
    start_date = Date.new(year, month, 1)
    end_date   = start_date.next_month
  elsif year = options[:year]
    start_date = Date.new(year, 1, 1)
    end_date   = Date.new(year+1, 1, 1)
  end

  start_date ||= Timeframe.to_date(args[0])
  end_date ||= Timeframe.to_date(args[1])

  raise ArgumentError, "Please supply a start and end date, `#{args.map(&:inspect).to_sentence}' is not enough" if start_date.nil? or end_date.nil?
  raise ArgumentError, "Start date #{start_date} should be earlier than end date #{end_date}" if start_date > end_date

  @start_date, @end_date = start_date, end_date
end

Instance Attribute Details

#end_dateObject (readonly)

Returns the value of attribute end_date.



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

def end_date
  @end_date
end

#start_dateObject (readonly)

Returns the value of attribute start_date.



117
118
119
# File 'lib/timeframe.rb', line 117

def start_date
  @start_date
end

Class Method Details

.constrained_new(start_date, end_date, constraint) ⇒ Object

Construct a new Timeframe, but constrain it by another

Raises:

  • (ArgumentError)


23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/timeframe.rb', line 23

def constrained_new(start_date, end_date, constraint)
  start_date, end_date = make_dates start_date, end_date
  raise ArgumentError, 'Constraint must be a Timeframe' unless constraint.is_a? Timeframe
  raise ArgumentError, "Start date #{start_date} should be earlier than end date #{end_date}" if start_date > end_date
  if end_date <= constraint.start_date or start_date >= constraint.end_date
    new constraint.start_date, constraint.start_date
  elsif start_date.year == end_date.yesterday.year
    new(start_date, end_date) & constraint
  elsif start_date.year < constraint.start_date.year and constraint.start_date.year < end_date.yesterday.year
    constraint
  else
    new [constraint.start_date, start_date].max, [constraint.end_date, end_date].min
  end
end

.from_hash(hsh) ⇒ Object

Construct a new Timeframe from a hash with keys startDate and endDate



59
60
61
62
# File 'lib/timeframe.rb', line 59

def from_hash(hsh)
  hsh = hsh.symbolize_keys
  new hsh[:startDate], hsh[:endDate]
end

.from_iso8601(str) ⇒ Object

Construct a new Timeframe by parsing an ISO 8601 time interval string en.wikipedia.org/wiki/ISO_8601#Time_intervals



47
48
49
50
51
52
53
54
55
56
# File 'lib/timeframe.rb', line 47

def from_iso8601(str)
  delimiter = str.include?('/') ? '/' : '--'
  a_raw, b_raw = str.split delimiter
  if a_raw.blank? or b_raw.blank?
    raise ArgumentError, "Interval must be specified according to ISO 8601 <start>/<end>, <start>/<duration>, or <duration>/<end>."
  end
  a = Iso8601::A.new a_raw
  b = Iso8601::B.new b_raw
  new a.to_time(b), b.to_time(a)
end

.from_year(year) ⇒ Object

Construct a new Timeframe from a year.



65
66
67
# File 'lib/timeframe.rb', line 65

def from_year(year)
  new :year => year.to_i
end

.mid(number) ⇒ Object

Create a timeframe +/- number of years around today



39
40
41
42
43
# File 'lib/timeframe.rb', line 39

def mid(number)
  start_date = Time.now.today - number.years
  end_date = Time.now.today + number.years
  new start_date, end_date
end

.multiyear(*args) ⇒ Object

Deprecated



93
94
95
# File 'lib/timeframe.rb', line 93

def multiyear(*args) # :nodoc:
  new *args
end

.parse(input) ⇒ Object Also known as: interval, from_json

Automagically parse a Timeframe from either a String or a Hash



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/timeframe.rb', line 70

def parse(input)
  case input
  when ::Integer
    from_year input
  when ::Hash
    from_hash input
  when ::String
    str = input.strip
    if str.start_with?('{')
      from_hash MultiJson.load(str)
    elsif input =~ /\A\d\d\d\d\z/
      from_year input
    else
      from_iso8601 str
    end
  else
    raise ArgumentError, "Must be String or Hash"
  end
end

.this_yearObject

Shortcut method to return the Timeframe representing the current year (as defined by Time.now)



18
19
20
# File 'lib/timeframe.rb', line 18

def this_year
  new :year => Time.now.year
end

.to_date(v) ⇒ Object



97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/timeframe.rb', line 97

def to_date(v)
  case v
  when NilClass
    nil
  when Date
    v
  when Time
    v.to_date
  else
    Date.parse v
  end
end

Instance Method Details

#&(other_timeframe) ⇒ Object

Returns a timeframe representing the intersection of the given timeframes



232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/timeframe.rb', line 232

def &(other_timeframe)
  this_timeframe = self
  if other_timeframe == this_timeframe
    this_timeframe
  elsif this_timeframe.start_date > other_timeframe.start_date and this_timeframe.end_date < other_timeframe.end_date
    this_timeframe
  elsif other_timeframe.start_date > this_timeframe.start_date and other_timeframe.end_date < this_timeframe.end_date
    other_timeframe
  elsif this_timeframe.start_date >= other_timeframe.end_date or this_timeframe.end_date <= other_timeframe.start_date
    nil
  else
    Timeframe.new [this_timeframe.start_date, other_timeframe.start_date].max, [this_timeframe.end_date, other_timeframe.end_date].min
  end
end

#/(other_timeframe) ⇒ Object

Returns the fraction (as a Float) of another Timeframe that this Timeframe represents

Raises:

  • (ArgumentError)


248
249
250
251
# File 'lib/timeframe.rb', line 248

def /(other_timeframe)
  raise ArgumentError, 'You can only divide a Timeframe by another Timeframe' unless other_timeframe.is_a? Timeframe
  self.days.to_f / other_timeframe.days.to_f
end

#==(other) ⇒ Object Also known as: eql?

Returns true when this timeframe is equal to the other timeframe



190
191
192
193
194
# File 'lib/timeframe.rb', line 190

def ==(other)
  # puts "checking to see if #{self} is equal to #{other}" if Emitter::DEBUG
  return false unless other.is_a?(Timeframe)
  start_date == other.start_date and end_date == other.end_date
end

#as_jsonObject



297
298
299
# File 'lib/timeframe.rb', line 297

def as_json(*)
  iso8601
end

#covered_by?(*timeframes) ⇒ Boolean

Returns true if the union of the given Timeframes includes the Timeframe

Returns:

  • (Boolean)


285
286
287
# File 'lib/timeframe.rb', line 285

def covered_by?(*timeframes)
  gaps_left_by(*timeframes).empty?
end

#crop(container) ⇒ Object

Crop a Timeframe by another Timeframe

Raises:

  • (ArgumentError)


254
255
256
257
# File 'lib/timeframe.rb', line 254

def crop(container)
  raise ArgumentError, 'You can only crop a timeframe by another timeframe' unless container.is_a? Timeframe
  self.class.new [start_date, container.start_date].max, [end_date, container.end_date].min
end

#datesObject



308
309
310
311
312
313
314
315
316
# File 'lib/timeframe.rb', line 308

def dates
  dates = []
  cursor = start_date
  while cursor < end_date
    dates << cursor
    cursor = cursor.succ
  end
  dates
end

#daysObject

The number of days in the timeframe

Timeframe.new(Date.new(2007, 11, 1), Date.new(2007, 12, 1)).days #=> 30
Timeframe.new(:month => 1).days #=> 31
Timeframe.new(:year => 2004).days #=> 366


165
166
167
# File 'lib/timeframe.rb', line 165

def days
  (end_date - start_date).to_i
end

#ending_no_later_than(date) ⇒ Object

Crop a Timeframe to end no later than the provided date.



221
222
223
224
225
226
227
228
229
# File 'lib/timeframe.rb', line 221

def ending_no_later_than(date)
  if end_date < date
    self
  elsif start_date >= date
    nil
  else
    Timeframe.new start_date, date
  end
end

#first_days_of_monthsObject



318
319
320
321
322
323
324
325
326
# File 'lib/timeframe.rb', line 318

def first_days_of_months
  dates = []
  cursor = start_date.beginning_of_month
  while cursor < end_date
    dates << cursor
    cursor = cursor >> 1
  end
  dates
end

#fromObject

Deprecated



329
330
331
# File 'lib/timeframe.rb', line 329

def from # :nodoc:
  @start_date
end

#gaps_left_by(*timeframes) ⇒ Object

Returns an array of Timeframes representing the gaps left in the Timeframe after removing all given Timeframes



260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/timeframe.rb', line 260

def gaps_left_by(*timeframes)
  # remove extraneous timeframes
  timeframes.reject! { |t| t.end_date <= start_date }
  timeframes.reject! { |t| t.start_date >= end_date }
  
  # crop timeframes
  timeframes.map! { |t| t.crop self }

  # remove proper subtimeframes
  timeframes.reject! { |t| timeframes.detect { |u| u.proper_include? t } }

  # escape
  return [self] if  timeframes.empty?

  timeframes.sort! { |x, y| x.start_date <=> y.start_date }
  
  a = [ start_date ] + timeframes.collect(&:end_date)
  b = timeframes.collect(&:start_date) + [ end_date ]

  a.zip(b).map do |gap|
    Timeframe.new(*gap) if gap[1] > gap[0]
  end.compact
end

#hashObject

Calculates a hash value for the Timeframe, used for equality checking and Hash lookups.



198
199
200
# File 'lib/timeframe.rb', line 198

def hash
  start_date.hash + end_date.hash
end

#include?(obj) ⇒ Boolean

Returns true when a Date or other Timeframe is included in this Timeframe

Returns:

  • (Boolean)


170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/timeframe.rb', line 170

def include?(obj)
  # puts "checking to see if #{date} is between #{start_date} and #{end_date}" if Emitter::DEBUG
  case obj
  when Date
    (start_date...end_date).include?(obj)
  when Time
    # (start_date...end_date).include?(Date.parse(obj))
    raise "this wasn't previously supported, but it could be"
  when Timeframe
    start_date <= obj.start_date and end_date >= obj.end_date
  end
end

#inspectObject

:nodoc:



156
157
158
# File 'lib/timeframe.rb', line 156

def inspect # :nodoc:
  "<Timeframe(#{object_id}) #{days} days starting #{start_date} ending #{end_date}>"
end

#iso8601Object Also known as: to_s, to_param

An ISO 8601 “time interval” like YYYY-MM-DD/YYYY-MM-DD



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

def iso8601
  "#{start_date.iso8601}/#{end_date.iso8601}"
end

#last_yearObject

Returns the same Timeframe, only a year earlier



290
291
292
293
294
295
# File 'lib/timeframe.rb', line 290

def last_year
  self.class.new(
    Date.new(start_date.year - 1, start_date.month, start_date.day),
    Date.new(end_date.year - 1, end_date.month, end_date.day)
  )
end

#monthsObject

Returns an Array of month-long Timeframes. Partial months are not included by default. stackoverflow.com/questions/1724639/iterate-every-month-with-date-objects



210
211
212
213
214
215
216
217
218
# File 'lib/timeframe.rb', line 210

def months
  memo = []
  ptr = start_date
  while ptr <= end_date do
    memo.push(Timeframe.new(:year => ptr.year, :month => ptr.month) & self)
    ptr = ptr >> 1
  end
  memo.flatten.compact
end

#proper_include?(other_timeframe) ⇒ Boolean

Returns true when the parameter Timeframe is properly included in the Timeframe

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)


184
185
186
187
# File 'lib/timeframe.rb', line 184

def proper_include?(other_timeframe)
  raise ArgumentError, 'Proper inclusion only makes sense when testing other Timeframes' unless other_timeframe.is_a? Timeframe
  (start_date < other_timeframe.start_date) and (end_date > other_timeframe.end_date)
end

#toObject

Deprecated



334
335
336
# File 'lib/timeframe.rb', line 334

def to # :nodoc:
  @end_date
end

#yearObject

Returns the relevant year as a Timeframe

Raises:

  • (ArgumentError)


203
204
205
206
# File 'lib/timeframe.rb', line 203

def year
  raise ArgumentError, 'Timeframes that cross year boundaries are dangerous during Timeframe#year' unless start_date.year == end_date.yesterday.year
  Timeframe.new :year => start_date.year
end