ruby-on-rails Ruby:创建日期范围

klh5stk1  于 2023-04-08  发布在  Ruby
关注(0)|答案(9)|浏览(182)

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

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

是否有一个优雅的解决方案?

r1wp621o

r1wp621o1#

使用@CaptainPete的base,我将其修改为使用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]
h7appiyu

h7appiyu2#

没有舍入错误,一个Range调用.succ方法来枚举序列,这不是您想要的。
不是一行程序,而是一个简短的helper函数就足够了:

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
2j4z5cfb

2j4z5cfb3#

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

之前的方法

...是在Object上使用DateRange < Range类+DateRange“构造函数”添加#every,然后在内部将时间转换为整数,在step秒内单步执行它们。这最初不适用于时区。添加了对时区的支持,但后来发现另一个问题,即某些步骤持续时间是动态的(例如1.month)。
阅读了Rubinius' Range implementation之后,我们就明白了如何添加对ActiveSupport::Duration的支持;非常感谢DanNguyen的#advance技巧和调试,感谢Rubinius对Range#step的实现,因为它编写得非常漂亮:D

更新2015-08-14

  • 这个补丁was not merged into Rails/ActiveSupport。你应该坚持使用#advance来循环for。如果你得到can't iterate from Time或者类似的东西,那么就使用这个补丁,或者避免使用Range
  • 更新补丁以反映Rails 4+ prepend风格而不是alias_method_chain
7nbnzgx9

7nbnzgx94#

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

请注意,此方法考虑范围min和max

2vuwiymt

2vuwiymt5#

一小时是一天的二十四分之一,所以你可以

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

1/24r是一个Rational,比Float更精确。

zzlelutf

zzlelutf6#

如果希望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
p8ekf7hl

p8ekf7hl7#

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

kdfy810k

kdfy810k8#

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

yqhsw0fo9#

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
....

**P.S.**原始问题是2013年的,所以我用docker镜像ruby:2.0.0-slim测试了一下。Ruby 2.0.0是2012年发布的,运行得很好。

相关问题