问题描述
使用红宝石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?
。
我可以轻松地通过一些手动的丑陋检查来使用例工作,但这不是优雅的红宝石方式。我在这里想念什么吗?或者这应该是尚不存在的功能?
解决方法
使用Range#include?
,实际上是在迭代范围,比较范围中的每个元素是否等于被测试的元素。仅在数字范围内,它会在内部进行优化以表现出您显然期望的那样。引用the docs:
如果
true
是范围的元素,则返回obj
,否则返回false
。如果begin
和end
是数字,则根据值的大小进行比较。
因此,您可能想在这里使用Range#cover?
而不是strict mode,它仅检查范围的边界(并且仅在数字边界上与Range#include?
相同):
如果
Range#include?
在范围的true
和obj
之间,则返回begin
。当
end
为begin <= obj <= end
时测试exclude_end?
,而当false
为begin <= obj < end
时测试exclude_end?
。[...]
如果范围的
true
值大于false
值,则返回begin
。如果对end
的内部调用之一返回false
(表示对象不可比较),则还返回<=>
。
通过您的示例,nil
做正确的事情:
Range#cover?
,
TL; DR
这要么是在无限范围内比较Date对象的错误,要么是某些迭代器在无限范围内如何工作的已知问题。我在下面提供了解释和一些解决方法。
分析与解释
Ruby的beginless and endless Range objects有一些令人惊讶但已被记录的行为。该文档称它们为“实现细节”,并对其进行了如下描述:
- 开始范围的
begin
和无限范围的end
是nil
;each
范围广,会引发异常;each
的范围无限,它枚举了无限序列(与Enumerable#take_while或类似方法结合使用可能很有用);(1..)
和(1...)
不相等,尽管从技术上讲代表相同的顺序。
结果是,您在某种程度上受制于如何为给定对象类型或方法实现迭代。在实用上,似乎对Integer范围进行了一些优化,以允许代码如下:
(1..).include? 999_999_999
#=> true
(1..).to_a
#=> RangeError (cannot convert endless range to an array)
可以快速执行(或失败),但是您的特定代码(从实际意义上来说)正在尝试使无穷大。由于Date#yesterday不是Ruby的核心方法,因此无论哪种mixin猴子修补了Date类,如何构造Range都可能是一个问题。但是,即使将其重构为原始Ruby 2.7.1,((Date.today - 1)..).include?(Date.today - 2)
也会挂起。
解决行为
对于Ruby Core团队来说,以上行为是错误还是设计选择是一个问题。但是,您可以通过checking bounds rather than iterating非常轻松地解决它。如果必须进行迭代,则不要尝试对无穷大进行迭代。例如:
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,825,000天才发现您没有匹配项,则需要花费大量时间。另一方面,以下内容几乎立即返回:
(two_days_ago .. distant_future).include? yesterday
#=> true
每种语言都有其错误和粗糙之处。这似乎是其中之一。无论哪种方式,出于实用主义的考虑,我都建议避免在Beginless / endless Date范围内进行迭代。