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

VERSION =
'0.2.1'

Instance Attribute Summary (collapse)

Class Method Summary (collapse)

Instance Method Summary (collapse)

Constructor Details

- (Timeframe) initialize(*args)

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

- (Object) end_date (readonly)

Returns the value of attribute end_date



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

def end_date
  @end_date
end

- (Object) start_date (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

+ (Object) constrained_new(start_date, end_date, constraint)

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

+ (Object) from_hash(hsh)

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

+ (Object) from_iso8601(str)

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

+ (Object) from_year(year)

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

+ (Object) mid(number)

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

+ (Object) multiyear(*args)

Deprecated



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

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

+ (Object) parse(input) 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

+ (Object) this_year

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

- (Object) &(other_timeframe)

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

- (Object) /(other_timeframe)

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

- (Object) ==(other) 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

- (Object) as_json



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

def as_json(*)
  iso8601
end

- (Boolean) covered_by?(*timeframes)

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

- (Object) crop(container)

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

- (Object) dates



292
293
294
295
296
297
298
299
300
# File 'lib/timeframe.rb', line 292

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

- (Object) days

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

- (Object) ending_no_later_than(date)

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

- (Object) first_days_of_months



302
303
304
305
306
307
308
309
310
# File 'lib/timeframe.rb', line 302

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

- (Object) from

Deprecated



313
314
315
# File 'lib/timeframe.rb', line 313

def from # :nodoc:
  @start_date
end

- (Object) gaps_left_by(*timeframes)

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

- (Object) hash

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

- (Boolean) include?(obj)

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

- (Object) inspect

: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

- (Object) iso8601 Also known as: to_s, to_param

An ISO 8601 "time interval" like YYYY-MM-DD/YYYY-MM-DD



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

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

- (Object) last_year

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

- (Object) months

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

- (Boolean) proper_include?(other_timeframe)

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

- (Object) to

Deprecated



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

def to # :nodoc:
  @end_date
end

- (Object) year

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