10

I have a daterange (from, to) that i want loop through an different intervals (daily, weekly, monthly, ...)

How can i loop through this dateranges?

Update

Thanks for your answers, i came up with the following:

interval = 'week' # month, year
start = from
while start < to
  stop  = start.send("end_of_#{interval}")
  if stop > to
    stop = to
  end
  logger.debug "Interval from #{start.inspect} to #{stop.inspect}"
  start = stop.send("beginning_of_#{interval}")
  start += 1.send(interval)
end

This will loop through a date range with intervals week, month or year and respects the beginning and end of the given interval.

Since i did not mention this in my question i choosed the answer that pushed me into the right direction.

tonymarschall
  • 3,862
  • 3
  • 29
  • 52

5 Answers5

7

Loop until the from date plus 1.day, 1.week, or 1.month is greater than the to date?

 > from = Time.now
 => 2012-05-12 09:21:24 -0400 
 > to = Time.now + 1.month + 2.week + 3.day
 => 2012-06-29 09:21:34 -0400 
 > tmp = from
 => 2012-05-12 09:21:24 -0400 
 > begin
?>   tmp += 1.week
?>   puts tmp
?> end while tmp <= to
2012-05-19 09:21:24 -0400
2012-05-26 09:21:24 -0400
2012-06-02 09:21:24 -0400
2012-06-09 09:21:24 -0400
2012-06-16 09:21:24 -0400
2012-06-23 09:21:24 -0400
2012-06-30 09:21:24 -0400
 => nil 
Dave Newton
  • 158,873
  • 26
  • 254
  • 302
7

In Ruby 1.9, I added my own method on Range for stepping through time ranges:

class Range
  def time_step(step, &block)
    return enum_for(:time_step, step) unless block_given?

    start_time, end_time = first, last
    begin
      yield(start_time)
    end while (start_time += step) <= end_time
  end
end

Then, you can call this like, e.g. (My example uses a Rails specific method: 15.minutes):

irb(main):001:0> (1.hour.ago..Time.current).time_step(15.minutes) { |time| puts time }
2012-07-01 21:07:48 -0400
2012-07-01 21:22:48 -0400
2012-07-01 21:37:48 -0400
2012-07-01 21:52:48 -0400
2012-07-01 22:07:48 -0400
=> nil

irb(main):002:0> (1.hour.ago..Time.current).time_step(15.minutes).map { |time| time.to_s(:short) }
=> ["01 Jul 21:10", "01 Jul 21:25", "01 Jul 21:40", "01 Jul 21:55", "01 Jul 22:10"]

Notice that this method uses the Ruby 1.9 convention where enumeration methods return an enumerator if no block is given, which allows you to string enumerators together.

UPDATE

I've added the Range#time_step method to my personal core_extensions "gem". If you'd like to utilize this in your Rails project, just add the following to your Gemfile:

gem 'core_extensions', github: 'pdobb/core_extensions'
pdobb
  • 17,688
  • 5
  • 59
  • 74
  • 1
    Nice, I should've found this answer sooner it would've saved me time! Wrote a very similar implementation for adding `ActiveSupport::Duration`s to `Range#step` http://stackoverflow.com/questions/19093487/ruby-create-range-of-dates/19094504#19094504 – captainpete Oct 17 '13 at 03:49
4

The succ method is deprecated in 1.9 range. Wanting to do the same thing by week, I came to this solution :

  def by_week(start_date, number_of_weeks)
    number_of_weeks.times.inject([]) { |memo, w| memo << start_date + w.weeks }
  end

This return an array of week in the interval. Easily adaptable for months.

tal
  • 3,423
  • 1
  • 25
  • 21
0

You have the step method on the Range object. http://ruby-doc.org/core-1.9.3/Range.html#method-i-step

Cristian Bica
  • 4,067
  • 27
  • 28
  • Can you provide an example? You can't iterate from time, so it's not just a matter of stepping via `1.week` etc. – Dave Newton May 12 '12 at 13:27
  • You can do some basic stuff here: (Date.current - 5.months .. Date.current).step(7){#code}, (Date.current - 5.months .. Date.current).step(1){#code} – Cristian Bica May 12 '12 at 13:53
  • I think you can iterate from time but it will be very slow as it will create an item in the range for each second – Cristian Bica May 12 '12 at 13:54
  • Not in 1.9 you can't, AFAICT, ultrahigh I didn't do it by a plain int. – Dave Newton May 12 '12 at 14:08
  • Yup ... from 1.9 you can't do ranges on Time. Range works by calling the method succ on the start value of the range and in 1.9 the succ method on time was deprecated... – Cristian Bica May 12 '12 at 19:49
0

Think about timestamps, they are plain integers and you use basic arithmetic on them. My solution is creating the array with the help of timestamps.

start_date = Time.now.to_datetime
end_date = Time.now.to_datetime + 1.year
interval = 10.years.in_days.to_i.days.in_seconds
# To make the first date as start date
epoch = start_date.to_i - interval

(start_date..end_date)
  .select {|time| (time.to_i - epoch) % interval == 0 }
  .each { |time| puts time }