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