Ruby:创建日期范围

时间:2013-09-30 11:49:54

标签: ruby-on-rails 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,用于处理几个月内可变天数等事情。
  • 理想情况下,表现会很好,但这不是主要要求。

有优雅的解决方案吗?

7 个答案:

答案 0 :(得分:24)

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

答案 1 :(得分:14)

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

答案 2 :(得分:14)

将持续时间支持添加到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等多部分持续时间

这增加了使用现有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“构造函数”添加Object,然后在内部将时间转换为整数,逐步执行在step秒内。这最初不适用于时区。增加了对时区的支持,但随后发现另一个问题,即一些步骤持续时间是动态的(例如1.month)。

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

更新2015-08-14

  • 此补丁was not merged into Rails/ActiveSupport。您应该使用for坚持#advance循环。如果您收到can't iterate from Time或类似的内容,请使用此修补程序,或者只是避免使用Range

  • 更新了补丁,以反映prepend上的Rails 4 + alias_method_chain样式。

答案 3 :(得分: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

答案 4 :(得分: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,你可能不需要为此实现数组,但我不知道你的目标是什么,所以我真的不能说。

答案 5 :(得分:2)

另一种解决方案是使用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]

请注意,此方法遵守最小和最大范围

答案 6 :(得分:1)

一个小时是一天的1/24,所以您可以这样做

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

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