Reputation: 9329
I'm looking for an elegant way to make a range of datetimes, e.g.:
def DateRange(start_time, end_time, period)
...
end
>> results = DateRange(DateTime.new(2013,10,10,12), DateTime.new(2013,10,10,14), :hourly)
>> puts results
2013-10-10:12:00:00
2013-10-10:13:00:00
2013-10-10:14:00:00
The step should be configurable, e.g. hourly, daily, monthly.
I'd like times
to be inclusive, i.e. include end_time
.
Additional requirements are:
:advance
, to handle things like variable number of days in months.Is there an elegant solution?
Upvotes: 31
Views: 43672
Reputation: 3388
Using @CaptainPete's base, I modified it to use the ActiveSupport::DateTime#advance
call. The difference comes into effect when the time intervals are non-uniform, such as `:month" and ":year"
require 'active_support/all'
class RailsDateRange < Range
# step is similar to DateTime#advance argument
def every(step, &block)
c_time = self.begin.to_datetime
finish_time = self.end.to_datetime
foo_compare = self.exclude_end? ? :< : :<=
arr = []
while c_time.send( foo_compare, finish_time) do
arr << c_time
c_time = c_time.advance(step)
end
return arr
end
end
# Convenience method
def RailsDateRange(range)
RailsDateRange.new(range.begin, range.end, range.exclude_end?)
end
My method also returns an Array
. For comparison's sake, I altered @CaptainPete's answer to also return an array:
RailsDateRange((4.years.ago)..Time.now).every(years: 1)
=> [Tue, 13 Oct 2009 11:30:07 -0400,
Wed, 13 Oct 2010 11:30:07 -0400,
Thu, 13 Oct 2011 11:30:07 -0400,
Sat, 13 Oct 2012 11:30:07 -0400,
Sun, 13 Oct 2013 11:30:07 -0400]
DateRange((4.years.ago)..Time.now).every(1.year)
=> [2009-10-13 11:30:07 -0400,
2010-10-13 17:30:07 -0400,
2011-10-13 23:30:07 -0400,
2012-10-13 05:30:07 -0400,
2013-10-13 11:30:07 -0400]
RailsDateRange((5.months.ago)..Time.now).every(months: 1)
=> [Mon, 13 May 2013 11:31:55 -0400,
Thu, 13 Jun 2013 11:31:55 -0400,
Sat, 13 Jul 2013 11:31:55 -0400,
Tue, 13 Aug 2013 11:31:55 -0400,
Fri, 13 Sep 2013 11:31:55 -0400,
Sun, 13 Oct 2013 11:31:55 -0400]
DateRange((5.months.ago)..Time.now).every(1.month)
=> [2013-05-13 11:31:55 -0400,
2013-06-12 11:31:55 -0400,
2013-07-12 11:31:55 -0400,
2013-08-11 11:31:55 -0400,
2013-09-10 11:31:55 -0400,
2013-10-10 11:31:55 -0400]
RailsDateRange((4.years.ago)..Time.now).every(years: 1)
=> [Tue, 13 Oct 2009 11:30:07 -0400,
Wed, 13 Oct 2010 11:30:07 -0400,
Thu, 13 Oct 2011 11:30:07 -0400,
Sat, 13 Oct 2012 11:30:07 -0400,
Sun, 13 Oct 2013 11:30:07 -0400]
DateRange((4.years.ago)..Time.now).every(1.year)
=> [2009-10-13 11:30:07 -0400,
2010-10-13 17:30:07 -0400,
2011-10-13 23:30:07 -0400,
2012-10-13 05:30:07 -0400,
2013-10-13 11:30:07 -0400]
Upvotes: 32
Reputation: 7038
Ruby standard library date
has a built-in method step
that we can use to achieve this:
require 'date'
array_of_datetimes = DateTime.parse("2021-09-01 13:00:00")
.step(DateTime.parse("2021-09-21 13:00:00"), (1.0/24))
.to_a
Let's break it down so it's easier to read
require 'date'
start_date = DateTime.parse("2021-09-01 13:00:00")
end_date = DateTime.parse("2021-09-21 13:00:00")
# Date#step expects a number representing a day.
# Let's convert into an hour
period = 1.0/24
array_of_datestimes = start_date.step(end_date, period).to_a
Output of puts array_of_datetime
....
2021-09-03T17:00:00+00:00
2021-09-03T18:00:00+00:00
2021-09-03T19:00:00+00:00
2021-09-03T20:00:00+00:00
2021-09-03T21:00:00+00:00
2021-09-03T22:00:00+00:00
....
P.S.
Original question is from 2013, so I tested this using docker image ruby:2.0.0-slim
. Ruby 2.0.0 was released in 2012 , and it works fine.
Upvotes: 3
Reputation: 80065
An hour is 1/24th of a day, so you could do
d1 = DateTime.now
d2 = d1 + 1
d1.step(d2, 1/24r){|d| p d}
1/24r
is a Rational, more exact than a Float.
Upvotes: 4
Reputation: 649
Another solution is to use uniq
method. Consider examples:
date_range = (Date.parse('2019-01-05')..Date.parse('2019-03-01'))
date_range.uniq { |d| d.month }
# => [Sat, 05 Jan 2019, Fri, 01 Feb 2019]
date_range.uniq { |d| d.cweek }
# => [Sat, 05 Jan 2019, Mon, 07 Jan 2019, Mon, 14 Jan 2019, Mon, 21 Jan 2019, Mon, 28 Jan 2019, Mon, 04 Feb 2019, Mon, 11 Feb 2019, Mon, 18 Feb 2019, Mon, 25 Feb 2019]
Note that this approach respects range min and max
Upvotes: 5
Reputation: 6222
Range#step
module RangeWithStepTime
def step(step_size = 1, &block)
return to_enum(:step, step_size) unless block_given?
# Defer to Range for steps other than durations on times
return super unless step_size.kind_of? ActiveSupport::Duration
# Advance through time using steps
time = self.begin
op = exclude_end? ? :< : :<=
while time.send(op, self.end)
yield time
time = step_size.parts.inject(time) { |t, (type, number)| t.advance(type => number) }
end
self
end
end
Range.prepend(RangeWithStepTime)
Range#step
method (no need for a sub-class, or convenience methods on Object
, though that was still fun)1.hour + 3.seconds
in step_size
This adds support for our duration to Range using the existing API. It allows you to use a regular range in the style that we expect to simply "just work".
# Now the question's invocation becomes even
# simpler and more flexible
step = 2.months + 4.days + 22.3.seconds
( Time.now .. 7.months.from_now ).step(step) do |time|
puts "It's #{time} (#{time.to_f})"
end
# It's 2013-10-17 13:25:07 +1100 (1381976707.275407)
# It's 2013-12-21 13:25:29 +1100 (1387592729.575407)
# It's 2014-02-25 13:25:51 +1100 (1393295151.8754072)
# It's 2014-04-29 13:26:14 +1000 (1398741974.1754072)
...was to add an #every
using a DateRange < Range
class + DateRange
"constructor" on Object
, then convert the times to integers internally, stepping through them in step
seconds. This didn't work for time zones originally. Support for time zones was added but then another issue was found with the fact some step durations are dynamic (eg 1.month
).
Reading Rubinius' Range
implementation it became clear how someone might add support for ActiveSupport::Duration
; so the approach was rewritten. Much thanks to Dan Nguyen for the #advance
tip and debugging around this, and to Rubinius' implementation of Range#step
for being beautifully written :D
This patch was not merged into Rails/ActiveSupport. You should stick to for
loops using #advance
. If you're getting can't iterate from Time
or something like that, then use this patch, or just avoid using Range
.
Updated patch to reflect Rails 4+ prepend
style over alias_method_chain
.
Upvotes: 15
Reputation: 15239
If you want to have n
values evenly spread between two dates you can
n = 8
beginning = Time.now - 1.day
ending = Time.now
diff = ending - beginning
result = []
(n).times.each do | x |
result << (beginning + ((x*diff)/(n-1)).seconds)
end
result
which gives
2015-05-20 16:20:23 +0100
2015-05-20 19:46:05 +0100
2015-05-20 23:11:48 +0100
2015-05-21 02:37:31 +0100
2015-05-21 06:03:14 +0100
2015-05-21 09:28:57 +0100
2015-05-21 12:54:40 +0100
2015-05-21 16:20:23 +0100
Upvotes: 3
Reputation: 1454
The Ice Cube - Ruby Date Recurrence Library would help here or look at their implementation? It has successfully covered every example in the RFC 5545 spec.
schedule = IceCube::Schedule.new(2013,10,10,12)
schedule.add_recurrence_rule IceCube::Rule.hourly.until(Time.new(2013,10,10,14))
schedule.all_occurrences
# => [
# [0] 2013-10-10 12:00:00 +0100,
# [1] 2013-10-10 13:00:00 +0100,
# [2] 2013-10-10 14:00:00 +0100
#]
Upvotes: 2
Reputation: 186
You can use DateTime.parse
on the start and end times to get a lock on the iterations you need to populate the array. For instance;
#Seconds
((DateTime.parse(@startdt) - DateTime.now) * 24 * 60 * 60).to_i.abs
#Minutes
((DateTime.parse(@startdt) - DateTime.now) * 24 * 60).to_i.abs
and so on. Once you have these values, you can loop through populating the array on whatever slice of time you want. I agree with @fotanus though, you probably shouldn't need to materialize an array for this, but I don't know what your goal is in doing so so I really can't say.
Upvotes: 2
Reputation: 6200
No rounding errors, a Range
calls the .succ
method to enumerate the sequence, which is not what you want.
Not a one-liner but, a short helper function will suffice:
def datetime_sequence(start, stop, step)
dates = [start]
while dates.last < (stop - step)
dates << (dates.last + step)
end
return dates
end
datetime_sequence(DateTime.now, DateTime.now + 1.day, 1.hour)
# [Mon, 30 Sep 2013 08:28:38 -0400, Mon, 30 Sep 2013 09:28:38 -0400, ...]
Note, however, this could be wildly inefficient memory-wise for large ranges.
Alternatively, you can use seconds since the epoch:
start = DateTime.now
stop = DateTime.now + 1.day
(start.to_i..stop.to_i).step(1.hour)
# => #<Enumerator: 1380545483..1380631883:step(3600 seconds)>
You'll have a range of integers, but you can convert back to a DateTime
easily:
Time.at(i).to_datetime
Upvotes: 19