Ruby:创建日期范围

Ste*_*fan 24 ruby ruby-on-rails

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

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
Run Code Online (Sandbox Code Playgroud)

该步骤应该是可配置的,例如每小时,每天,每月.

我想要times包容,即包括end_time.

其他要求是:

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

有优雅的解决方案吗?

Dan*_*yen 25

使用@ 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
Run Code Online (Sandbox Code Playgroud)

我的方法也返回一个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]
Run Code Online (Sandbox Code Playgroud)

按月计算

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]
Run Code Online (Sandbox Code Playgroud)

到了一年

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]
Run Code Online (Sandbox Code Playgroud)


kwa*_*ick 15

没有舍入错误,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, ...]
Run Code Online (Sandbox Code Playgroud)

但请注意,对于大范围而言,这可能在内存方面非常低效.


或者,您可以使用自纪元以来的秒数:

start = DateTime.now
stop  = DateTime.now + 1.day
(start.to_i..stop.to_i).step(1.hour)

# => #<Enumerator: 1380545483..1380631883:step(3600 seconds)>
Run Code Online (Sandbox Code Playgroud)

你将拥有一系列整数,但你可以DateTime轻松转换回来:

Time.at(i).to_datetime
Run Code Online (Sandbox Code Playgroud)


cap*_*ete 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)
Run Code Online (Sandbox Code Playgroud)

这种方法提供了

  • 对保留时区的隐含支持
  • 为已经存在的Range#step方法添加持续时间支持(不需要子类,或方便的方法Object,虽然这仍然很有趣)
  • 支持多部分持续时间像1.hour + 3.secondsstep_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)
Run Code Online (Sandbox Code Playgroud)

以前的方法

...是添加#every使用DateRange < Range类+ 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.


Dar*_*ide 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]
Run Code Online (Sandbox Code Playgroud)

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