Iterate over Ruby Time object with delta
Asked Answered
V

4

33

Is there a way to iterate over a Time range in Ruby, and set the delta?

Here is an idea of what I would like to do:

for hour in (start_time..end_time, hour)
    hour #=> Time object set to hour
end

You can iterate over the Time objects, but it returns every second between the two. What I really need is a way to set the offset or delta (such as minute, hour, etc.)

Is this built in to Ruby, or is there a decent plugin available?

Velate answered 1/2, 2009 at 17:53 Comment(0)
T
59

Prior to 1.9, you could use Range#step:

(start_time..end_time).step(3600) do |hour|
  # ...
end

However, this strategy is quite slow since it would call Time#succ 3600 times. Instead, as pointed out by dolzenko in his answer, a more efficient solution is to use a simple loop:

hour = start_time
while hour < end_time
  # ...
  hour += 3600
end

If you're using Rails you can replace 3600 with 1.hour, which is significantly more readable.

Ternopol answered 1/2, 2009 at 18:13 Comment(2)
Range#step does not work for Time objects in Ruby 1.9.2p180 and 1.9.3. It says: TypeError: can't iterate from Time.Truscott
It's easy enough to extent Range's step method for Time steps. I've written an answer to step dynamic intervals like 2.months + 33.seconds (if you're using ActiveSupport). See https://mcmap.net/q/452295/-ruby-create-range-of-dates/…. Internally, the proxy method uses this while-loop approach.Frenchify
T
40

If your start_time and end_time are actually instances of Time class then the solution with using the Range#step would be extremely inefficient since it would iterate over every second in this range with Time#succ. If you convert your times to integers the simple addition will be used but this way you will end up with something like:

(start_time.to_i..end_time.to_i).step(3600) do |hour|
  hour = Time.at(hour)     
  # ...
end

But this also can be done with simpler and more efficient (i.e. without all the type conversions) loop:

hour = start_time
begin
  # ...      
end while (hour += 3600) < end_time
Teammate answered 27/8, 2009 at 12:25 Comment(1)
@dylanfm Indeed. Since it doesn't look like the OP is going to change the accepted answer to this one, I have updated my answer to include this.Ternopol
M
18

Range#step method is very slow in this case. Use begin..end while, as dolzenko posted here.

You can define a new method:

  def time_iterate(start_time, end_time, step, &block)
    begin
      yield(start_time)
    end while (start_time += step) <= end_time
  end

then,

start_time = Time.parse("2010/1/1")
end_time = Time.parse("2010/1/31")
time_iterate(start_time, end_time, 1.hour) do |t|
  puts t
end

if in rails.

Mannie answered 30/12, 2009 at 2:22 Comment(0)
L
0

Here the common function for each case:

def split_time_by_periods(start_time, end_time, time_step)
  return to_enum(:split_time_by_periods, start_time, end_time, time_step) unless block_given?

  start_time = start_time.to_i
  end_time = end_time.to_i
  current_time = start_time

  while current_time < end_time
    period_end = [current_time + time_step, end_time].min
    yield(Time.at(current_time).utc, Time.at(period_end).utc)

    current_time += time_step
  end
end

Usage:

 split_time_by_periods(Time.now, Time.now + 1.hour + 30.minutes, 1.hour).to_a
 # => [[2023-07-26 08:00:26 UTC, 2023-07-26 09:00:26 UTC], [2023-07-26 09:00:26 UTC, 2023-07-26 09:30:26 UTC]]


split_time_by_periods(Time.now, Time.now + 1.day, 5.hours) do |period_start, period_end|
 # ...
end

split_time_by_periods(Time.now, Time.now + 1.day, 5.hours).map do |period_start, period_end|
 # ...
end
Loferski answered 26/7, 2023 at 8:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.