Ruby:创建日期范围

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19093487/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-02 22:46:54  来源:igfitidea点击:

Ruby: Create range of dates

ruby-on-railsruby

提问by Stefan

I'm looking for an elegant way to make a range of datetimes, e.g.:

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

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

The step should be configurable, e.g. hourly, daily, monthly.

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

I'd like timesto be inclusive, i.e. include end_time.

我想times具有包容性,即 include end_time

Additional requirements are:

附加要求是:

  • Original timezone should be preserved, i.e. if it's different from the local timezone, it should still be maintained.
  • Should use proper advance methods, e.g. Rails :advance, to handle things like variable number of days in months.
  • Ideally performance will be good, but that's not a primary requirement.
  • 应保留原始时区,即如果它与本地时区不同,则仍应保留。
  • 应该使用适当的高级方法,例如 Rails :advance,来处理诸如以月为单位的可变天数之类的事情。
  • 理想情况下,性能会很好,但这不是主要要求。

Is there an elegant solution?

有没有优雅的解决方案?

回答by Dan Nguyen

Using @CaptainPete's base, I modified it to use the ActiveSupport::DateTime#advancecall. The difference comes into effect when the time intervals are non-uniform, such as `:month" and ":year"

使用@CaptainPete 的基础,我修改了它以使用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

My method also returns an Array. For comparison's sake, I altered @CaptainPete's answer to also return an array:

我的方法也返回一个Array. 为了比较起见,我修改了@CaptainPete 的答案以返回一个数组:

By hour

按小时

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]

By month

按月

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]

By year

按年份

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]

回答by kwarrick

No rounding errors, a Rangecalls the .succmethod to enumerate the sequence, which is not what you want.

没有舍入错误,aRange调用.succ方法枚举序列,这不是你想要的。

Not a one-liner but, a short helper function will suffice:

不是单行的,但是一个简短的辅助函数就足够了:

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

Note, however, this could be wildly inefficient memory-wise for large ranges.

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



Alternatively, you can use seconds since the epoch:

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

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

# => #<Enumerator: 1380545483..1380631883:step(3600 seconds)>

You'll have a range of integers, but you can convert back to a DateTimeeasily:

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

Time.at(i).to_datetime

回答by captainpete

Add duration support to Range#step

添加持续时间支持 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)

This approach affords

这种方法提供

  • Implicit support for preserving time-zone
  • Adds duration support to the already-present Range#stepmethod (no need for a sub-class, or convenience methods on Object, though that was still fun)
  • Supports multi-part durations like 1.hour + 3.secondsin step_size
  • 隐式支持保留时区
  • 为已经存在的Range#step方法添加持续时间支持(不需要子类或 上的便利方法Object,尽管这仍然很有趣)
  • 支持多部分持续时间像1.hour + 3.secondsstep_size

This adds support for our duration to Range using the existing API. It allows you to use a regular range in the style that we expect to simply "just work".

这增加了对使用现有 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)

The previous approach

以前的做法

...was to add an #everyusing a DateRange < Rangeclass + DateRange"constructor" on Object, then convert the times to integers internally, stepping through them in stepseconds. This didn't work for time zones originally. Support for time zones was added but then another issue was found with the fact some step durations are dynamic (eg 1.month).

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

Reading Rubinius' Rangeimplementationit became clear how someone might add support for ActiveSupport::Duration; so the approach was rewritten. Much thanks to Dan Nguyen for the #advancetip and debugging around this, and to Rubinius' implementation of Range#stepfor being beautifully written :D

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

Update 2015-08-14

更新 2015-08-14

  • This patch was notmerged into Rails/ActiveSupport. You should stick to forloops using #advance. If you're getting can't iterate from Timeor something like that, then use this patch, or just avoid using Range.

  • Updated patch to reflect Rails 4+ prependstyle over alias_method_chain.

  • 这个补丁不能合并到Rails /的ActiveSupport。你应该坚持for使用循环#advance。如果你得到can't iterate from Time或类似的东西,然后使用这个补丁,或者只是避免使用Range.

  • 更新补丁以反映 Rails 4+prepend风格alias_method_chain

回答by xxjjnn

If you want to have nvalues evenly spread between two dates you can

如果您想让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

which gives

这使

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

回答by Darkside

Another solution is to use uniqmethod. Consider examples:

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

Note that this approach respects range min and max

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

回答by Kevin Monk

The Ice Cube - Ruby Date Recurrence Librarywould help here or look at their implementation? It has successfully covered every example in the RFC 5545spec.

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

回答by user401093

You can use DateTime.parseon the start and end times to get a lock on the iterations you need to populate the array. For instance;

您可以使用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

and so on. Once you have these values, you can loop through populating the array on whatever slice of time you want. I agree with @fotanus though, you probably shouldn't need to materialize an array for this, but I don't know what your goal is in doing so so I really can't say.

等等。一旦你有了这些值,你就可以在你想要的任何时间片上循环填充数组。不过我同意@fotanus,你可能不需要为此具体化一个数组,但我不知道你这样做的目标是什么,所以我真的不能说。

回答by steenslag

An hour is 1/24th of a day, so you could do

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

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

1/24ris a Rational, more exact than a Float.

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