Подтвердить что ты не робот

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, чтобы обрабатывать такие вещи, как переменное число дней в месяцах.
  • Идеальная производительность будет хорошей, но это не основное требование.

Есть ли элегантное решение?

4b9b3361

Ответ 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
#]