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 =
'0.2.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)


121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/timeframe.rb', line 121

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 = args.shift.to_date if start_date.nil? and args.any?
  end_date = args.shift.to_date if end_date.nil? and args.any?

  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.



105
106
107
# File 'lib/timeframe.rb', line 105

def end_date
  @end_date
end

#start_dateObject (readonly)

Returns the value of attribute start_date.



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

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

Instance Method Details

#&(other_timeframe) ⇒ Object

Returns a timeframe representing the intersection of the given timeframes



219
220
221
222
223
224
225
226
227
228
229
230
231
232
# File 'lib/timeframe.rb', line 219

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)


235
236
237
238
# File 'lib/timeframe.rb', line 235

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



177
178
179
180
181
# File 'lib/timeframe.rb', line 177

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



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

def as_json(*)
  iso8601
end

#covered_by?(*timeframes) ⇒ Boolean

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

Returns:

  • (Boolean)


272
273
274
# File 'lib/timeframe.rb', line 272

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

#crop(container) ⇒ Object

Crop a Timeframe by another Timeframe

Raises:

  • (ArgumentError)


241
242
243
244
# File 'lib/timeframe.rb', line 241

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



296
297
298
299
300
301
302
303
304
# File 'lib/timeframe.rb', line 296

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


152
153
154
# File 'lib/timeframe.rb', line 152

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.



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

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



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

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



317
318
319
# File 'lib/timeframe.rb', line 317

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



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

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.



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

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)


157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/timeframe.rb', line 157

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?(obj.to_date)
    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:



143
144
145
# File 'lib/timeframe.rb', line 143

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



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

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

#last_yearObject

Returns the same Timeframe, only a year earlier



277
278
279
# File 'lib/timeframe.rb', line 277

def last_year
  self.class.new((start_date - 1.year), (end_date - 1.year))
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



197
198
199
200
201
202
203
204
205
# File 'lib/timeframe.rb', line 197

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)


171
172
173
174
# File 'lib/timeframe.rb', line 171

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



322
323
324
# File 'lib/timeframe.rb', line 322

def to # :nodoc:
  @end_date
end

#to_jsonObject



281
282
283
# File 'lib/timeframe.rb', line 281

def to_json(*)
  iso8601
end

#yearObject

Returns the relevant year as a Timeframe

Raises:

  • (ArgumentError)


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

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