Ruby: создание диапазона дат
Я ищу элегантный способ сделать ряд дат, например:
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
Шаг должен быть настраиваемым, например. ежечасно, ежедневно, ежемесячно.
Я хотел бы, чтобы times
был включен, т.е. включал end_time
.
Дополнительные требования:
- Исходный часовой пояс должен быть сохранен, т.е. если он отличается от локального часового пояса, он все равно должен поддерживаться.
- Должны использовать надлежащие предварительные методы, например. Rails
:advance
, чтобы обрабатывать такие вещи, как переменное число дней в месяцах.
- Идеальная производительность будет хорошей, но это не основное требование.
Есть ли элегантное решение?
Ответы
Ответ 1
Используя базу @CaptainPete, я изменил ее, чтобы использовать вызов ActiveSupport::DateTime#advance
. Разница вступает в силу, когда временные интервалы неравномерны, такие как ": месяц" и "год"
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
Мой метод также возвращает Array
. Для сравнения, я изменил ответ @CaptainPete, чтобы вернуть массив:
По часам
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]
Ответ 2
Нет ошибок округления, a Range
вызывает метод .succ
для перечисления последовательности, которая не является тем, что вы хотите.
Не один лайнер, но достаточно короткой вспомогательной функции:
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, ...]
Обратите внимание, однако, это может быть крайне неэффективно по памяти для больших диапазонов.
В качестве альтернативы вы можете использовать секунды с эпохи:
start = DateTime.now
stop = DateTime.now + 1.day
(start.to_i..stop.to_i).step(1.hour)
# => #<Enumerator: 1380545483..1380631883:step(3600 seconds)>
У вас будет целый ряд целых чисел, но вы можете легко конвертировать обратно в DateTime
:
Time.at(i).to_datetime
Ответ 3
Добавить поддержку продолжительности 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
(нет необходимости в подклассе или методах удобства на Object
, хотя это все еще было весело)
- Поддержка многочастных длительностей, таких как
1.hour + 3.seconds
в step_size
Это добавляет поддержку нашей продолжительности в Range с использованием существующего API. Он позволяет использовать регулярный диапазон в стиле, который мы ожидаем просто "просто работать".
# Now the question 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 #{time} (#{time.to_f})"
end
# It 2013-10-17 13:25:07 +1100 (1381976707.275407)
# It 2013-12-21 13:25:29 +1100 (1387592729.575407)
# It 2014-02-25 13:25:51 +1100 (1393295151.8754072)
# It 2014-04-29 13:26:14 +1000 (1398741974.1754072)
Предыдущий подход
... должен был добавить #every
с помощью конструктора DateRange < Range
class + DateRange
"на Object
, а затем преобразовать время в целые числа внутри, пройдя через них в step
секунды. Первоначально это не работало для часовых поясов. Была добавлена поддержка часовых поясов, но затем была обнаружена еще одна проблема с тем, что некоторые временные периоды являются динамическими (например, 1.month
).
Чтение реализация Rubinius Range
стало ясно, как кто-то может добавить поддержку ActiveSupport::Duration
; поэтому подход был переписан. Огромное спасибо Dan Nguyen за подсказку #advance
и отладку по этому поводу, а также реализацию Rubinius Range#step
для красивого написания: D
Обновление 2015-08-14
-
Этот патч не был объединен с Rails/ActiveSupport. Вы должны придерживаться циклов for
, используя #advance
. Если вы получаете can't iterate from Time
или что-то в этом роде, используйте этот патч или просто не используйте Range
.
-
Обновлен патч, чтобы отразить стиль Rails 4+ prepend
над alias_method_chain
.
Ответ 4
Если вы хотите, чтобы значения n
равномерно распределялись между двумя датами, вы можете
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
который дает
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
Ответ 5
Вы можете использовать DateTime.parse
в начале и в конце, чтобы получить блокировку итераций, необходимых для заполнения массива. Например;
#Seconds
((DateTime.parse(@startdt) - DateTime.now) * 24 * 60 * 60).to_i.abs
#Minutes
((DateTime.parse(@startdt) - DateTime.now) * 24 * 60).to_i.abs
и т.д. Когда у вас есть эти значения, вы можете циклически заполнить массив на любой фрагмент времени, который вы хотите. Я согласен с @fotanus, хотя вам, вероятно, не нужно будет материализовать массив для этого, но я не знаю, какова ваша цель в этом, поэтому я действительно не могу сказать.
Ответ 6
Ice Cube - Ruby Date Recurrence Library поможет здесь или посмотрит на их реализацию? Он успешно рассмотрел каждый пример в спецификации RFC 5545.
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
#]