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 ,来处理像月份可变天数这样的事情。
  • 理想情况下,性能会很好,但这不是主要要求。

有优雅的解决方案吗?

使用@ 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] 

没有舍入错误, 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) # => # 

您将拥有一系列整数,但您可以轻松地转换回DateTime

 Time.at(i).to_datetime 

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上的便捷方法,尽管这仍然很有趣)
  • step_size支持多部分持续时间,如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) 

以前的方法

…是在#every使用DateRange < Range类+ DateRange “构造函数”添加#every ,然后在内部将时间转换为整数,在step秒内逐步执行它们。 这最初对时区不起作用。 增加了对时区的支持,但随后发现另一个问题,即一些步骤持续时间是动态的(例如1.month )。

阅读Rubinius的Range实现,很明显有人可能会添加对ActiveSupport::Duration支持; 所以这个方法被改写了。 非常感谢Dan Nguyen的#advance提示和调试,以及Rubinius实现Range#step的精美编写:D

更新2015-08-14

  • 此修补程序合并到Rails / ActiveSupport中 。 你应该坚持使用#advance循环。 如果你can't iterate from Time或类似的东西can't iterate from Time ,那么使用这个补丁,或者只是避免使用Range

  • 更新了补丁以反映Rails 4 + prepend样式而不是alias_method_chain

如果你想在两个日期之间均匀分布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 

您可以在开始和结束时间使用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,你可能不需要为此实现数组,但我不知道你的目标是什么,所以我真的不能说。

Ice Mube – 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 #]