无起点的日期范围不允许包含?

问题描述

使用红宝石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。如果beginend是数字,则根据值的大小进行比较。

因此,您可能想在这里使用Range#cover?而不是strict mode,它仅检查范围的边界(并且仅在数字边界上与Range#include?相同):

如果Range#include?在范围的trueobj之间,则返回begin

endbegin <= obj <= end时测试exclude_end?,而当falsebegin <= obj < end时测试exclude_end?

[...]

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

通过您的示例,nil做正确的事情:

Range#cover?
,

TL; DR

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

分析与解释

Ruby的beginless and endless Range objects有一些令人惊讶但已被记录的行为。该文档称它们为“实现细节”,并对其进行了如下描述:

  • 开始范围的begin和无限范围的endnil;
  • 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. 缩短日期范围,减少潜在的迭代次数。
  2. 检查日期范围附近的日期,所需的迭代次数较少。

例如,要遍历1,825,000天才发现您没有匹配项,则需要花费大量时间。另一方面,以下内容几乎立即返回:

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

每种语言都有其错误和粗糙之处。这似乎是其中之一。无论哪种方式,出于实用主义的考虑,我都建议避免在Beginless / endless Date范围内进行迭代。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...