28

我正在寻找一种优雅的方式来制作一系列日期时间,例如:

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具有包容性,即 include end_time

附加要求是:

  • 应保留原始时区,即如果它与本地时区不同,仍应保留。
  • 应该使用适当的高级方法,例如 Rails :advance,来处理月份可变天数之类的事情。
  • 理想情况下,性能会很好,但这不是主要要求。

有没有优雅的解决方案?

4

9 回答 9

31

使用@CaptainPete 的基础,我对其进行了修改以使用ActiveSupport::DateTime#advance调用。当时间间隔不均匀时,差异生效,例如 `:month" 和 ":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

我的方法还返回一个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]
于 2013-10-13T15:32:58.180 回答
18

没有舍入错误,aRange调用.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
于 2013-09-30T12:30:09.260 回答
15

添加持续时间支持到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(不需要子类或便捷方法 on Object,尽管这仍然很有趣)
  • 支持多部分持续时间1.hour + 3.seconds,如step_size

这增加了使用现有 API 对 Range 持续时间的支持。它允许您以我们期望简单“正常工作”的样式使用常规范围。

# 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)

以前的做法

...是添加一个#everyusing a DateRange < Rangeclass + DateRange"constructor" on 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

于 2013-09-30T12:38:13.200 回答
5

另一种解决方案是使用uniq方法。考虑示例:

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]

请注意,这种方法尊重范围最小值和最大值

于 2019-02-19T17:54:16.210 回答
4

一小时是一天的 1/24,所以你可以

d1 = DateTime.now
d2 = d1 + 1
d1.step(d2, 1/24r){|d| p d}

1/24r是有理数,比浮点数更精确。

于 2019-02-22T21:54:19.600 回答
3

如果您想让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
于 2015-05-21T15:21:13.150 回答
2

您可以使用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,您可能不需要为此实现一个数组,但我不知道您这样做的目标是什么,所以我真的不能说。

于 2013-09-30T12:15:26.157 回答
2

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
#]
于 2015-01-02T15:19:39.127 回答
1

Ruby 标准库date有一个内置方法step,我们可以使用它来实现这一点:

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

让我们把它分解一下,这样更容易阅读

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

的输出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
....

PS 原始问题来自2013年,所以我使用 docker image 对此进行了测试ruby:2.0.0-slim。Ruby 2.0.0 于 2012 年发布,运行良好。

于 2021-09-23T22:56:26.210 回答