0

ruby 2.7 引入了无始范围。现在你可以拥有:

(..5)
(5..10)
(10..)

使用整数,.include?按预期工作:

(..5).include?(6) # false
(..5).include?(5) # true
(..5).include?(2) # true
(..5).include?(-100) # true

但是,这同样不适用于日期范围:

(..Date.tomorrow).include?(Date.today) # RangeError (cannot get the first element of beginless range)

有趣的是,它反过来工作:

(Date.yesterday..).include?(Date.today) # true

最后:

(Date.yesterday..).include?(Date.today - 2.days) # Seems to loop forever.

这是一种很奇怪的行为。所有 3 个案例都带来了不同的结果,其中只有 1 个实际按预期工作。

我的意思是,我想如果我们有一个具有某种“连续”逻辑的范围是可以理解的,它可能很难检查是否包含。但是像 Date 这样相对简单的类至少应该可以工作。无论如何,日期几乎就像一个整数。甚至 Float 也可以做到这一点,所以我不明白为什么 Date 或 DateTime 不应该。

我的用例是数据库可能会为我正在查询的 2 个日期提供 nil 。这些是我想在一个范围内使用的开始日期和结束日期,但我不能确定其中一个可能不是 nil,这对我的逻辑来说很好,但这会导致一个无开始的范围,这可以不处理.include?

我可以通过一些手动的丑陋检查轻松地使我的用例工作,但这不是优雅的 ruby​​ 方式。我在这里错过了什么吗?或者这应该是一个尚不存在的功能?

4

2 回答 2

4

使用Range#include?,您实际上是在迭代范围,比较范围中的每个元素是否等于被测试的元素。只有数字范围,这在内部进行了优化,以按照您显然期望的方式运行。引用文档

true如果obj是范围的元素,则返回,false否则。如果beginend是数字,则根据值的大小进行比较。

因此,Range#include?您可能不想在Range#cover?这里使用它只检查范围的边界(并且它的工作方式与Range#include?仅使用数字边界相同):

true如果在范围的和obj之间,则返回。beginend

这测试begin <= obj <= end什么时候exclude_end?falsebegin <= obj < end什么时候exclude_end?true

[...]

false如果begin范围的值大于该值,则返回end。如果对返回false的内部调用之一(表示对象不可比较),也会<=>返回。nil

用你的例子,Range#cover?做正确的事:

(..Date.tomorrow).cover?(Date.today)
# => true

(Date.yesterday..).cover?(Date.today)
# => true

(Date.yesterday..).cover?(Date.today - 2.days)
#  => false
于 2020-09-21T20:02:02.893 回答
0

TL;博士

这要么是在无限范围中比较 Date 对象的错误,要么是某些迭代器如何与无限范围一起工作的已知问题。我在下面提供了解释和一些解决方法。

分析说明

Ruby 的无始无终的 Range 对象有一些令人惊讶但有据可查的行为。文档称它们为“实现细节”,并描述如下:

  • begin无始无终的范围和end无止境的范围是nil
  • eachbeginless range 引发异常;
  • each无限范围枚举无限序列(可能与 Enumerable#take_while 或类似方法结合使用);
  • (1..)并且(1...)不相等,尽管在技术上代表相同的序列。

因此,您在某种程度上受制于如何为给定的对象类型或方法实现迭代。务实地说,似乎对整数范围进行了一些优化,允许使用以下代码:

(1..).include? 999_999_999
#=> true

(1..).to_a
#=> RangeError (cannot convert endless range to an array)

快速执行(或失败),但您的特定代码(务实地说)试图具体化无穷大。由于 Date#yesterday 不是核心 Ruby 方法,因此 Range 是如何由任何 mixin 对您的 Date 类进行猴子补丁构建的问题也可能是一个问题。但是,即使重构为 vanilla Ruby 2.7.1,((Date.today - 1)..).include?(Date.today - 2)也会挂起。

围绕行为工作

上述行为是错误还是设计选择是 Ruby 核心团队的问题。但是,您可以通过检查边界而不是迭代来轻松解决它。如果你必须迭代,那么不要试图迭代无穷大。例如:

require 'date'

def distant_future
  # 5 millenia from today
  Date.today + (365 * 5_000)
end

def yesterday
  Date.today - 1
end

def two_days_ago
  yesterday - 1
end

# slow, but returns in about 0m1.046s on my system
(yesterday .. distant_future).include? two_days_ago

通过使用大但小于无穷大的东西作为范围的结束,您允许迭代返回。您可以通过两种方式提高性能:

  1. 缩短您的日期范围,减少潜在的迭代次数。
  2. 检查范围前面附近的日期,需要更少的迭代来匹配。

例如,迭代 1,825,000 天后才发现没有匹配项需要相当长的时间。另一方面,以下内容几乎立即返回:

(two_days_ago .. distant_future).include? yesterday
#=> true

每种语言都有它的错误和粗糙的边缘。这似乎是其中之一。无论哪种方式,为了实用主义,我都建议避免迭代开始/无尽的日期范围。

于 2020-09-21T20:24:34.087 回答