如何在Ruby中解构一个范围?

6

是否可以在Ruby中使用解构(Destructuring)来从范围(Range)中提取开头和结尾?

module PriceHelper
  def price_range_human( range )
    "$%s to $%s" % [range.begin, range.end].map(:number_to_currency)
  end
end

我知道可以使用数组强制转换作为一种非常糟糕的hack方法:

first, *center, last = *rng
"$%s to $%s" % [first, last].map(:number_to_currency)

但是有没有一种语法方式可以在不手动创建数组的情况下获取 beginend

min, max = (1..10)

本来就很棒。


2
您需要明确表示您只关心两个点范围,或者澄清您是想要“max”还是“last”,因为您似乎使用这两个词是可以互换的。 - Cary Swoveland
4个回答

5
你可以使用minmax来解构范围:
min, max = (1..10).minmax
min # => 1
max # => 10

如果您使用的是2.7之前的Ruby版本,请避免在大范围内使用此功能。


2
请注意,此操作返回的是范围的最大值和最小值,而不是范围的开始和结束。 - Stefan
还要注意,这种情况无法很好地处理无限范围。(-Float::INFINITY..0).minmax会引发TypeError错误,而(0..Float::INFINITY).minmax则永远不会返回值。 - Jay Mitchell
1
"minmax" 迭代整个范围,而 "[range.min, range.max]" 则不会迭代整个范围,并且可以很好地处理无限范围。因此,如果您的范围有大量(或无限)项,请避免使用 "minmax"。 - Jay Mitchell
1
在 Ruby 2.7 中,minmax 不再遍历范围并且与无限范围一起使用正常。https://rubyreferences.github.io/rubychanges//2.7.html#minmax-implementation-change - Sunny

4

起始和结束?我会使用:

foo = 1..2
foo.min # => 1
foo.max # => 2

尝试使用解构来处理范围是一个不好的想法。想象一下可能生成并且浪费 CPU 时间和内存的数组大小。如果你的范围以 Float::INFINITY 结尾,那么这实际上是 DOS 你自己代码的好方法。

end 不同于 max:在 1...10 中,end 是 10,但 max 是 9。

这是因为 start_val ... end_val 等价于 start_val .. (end_val - 1)

start_value = 1
end_value = 2

foo = start_value...end_value
foo.end # => 2
foo.max # => 1

foo = start_value..(end_value - 1)
foo.end # => 1
foo.max # => 1

max 反映了 Ruby 在遍历范围或测试是否包含在范围内时实际使用的值的现实情况。

我认为,end 应该反映范围内将被考虑的实际最大值,而不是在范围定义结束时使用的值,但我怀疑这种情况不会改变,否则会影响现有代码。

... 更加令人困惑,并导致维护问题增加,因此不建议使用它。


Range#minmaxendbegin不同。据我所知,max实际上将数组转换为范围并获取最后一个元素。 - max
minmax返回范围的起始和结束。文档中没有说明它被转换为数组。如果是这样,使用max时将永远不会返回1..Float::INFINITY。它们基本上与beginend相同,只是允许使用块。从源代码来看,beginend应该更快。 - the Tin Man
尝试使用块调用(1..Float::INFINITY).max - max
尽管在没有块的情况下调用它不会像1.9.3那样导致程序崩溃。 - max
1
@theTinMan:请注意,#end 不同于 #max:在 1...10 中,#end10,但 #max9。在 10..1 中,#end1,但 #maxnil。根据您的用例选择您想要的是 #begin#end(以及 #exclude_end?),还是 #min#max - Amadan
显示剩余3条评论

3

不,在Cary Swoveland、Weekly World News或其他小报证明我是错误的之前,我会继续认为没有任何证据表明答案是“否”;但很容易证明。

module RangeWithBounds
  refine Range do
    def bounds
      [self.begin, self.end]
    end
  end
end

module Test
  using RangeWithBounds
  r = (1..10)
  b, e = *r.bounds
  puts "#{b}..#{e}"
end

不过,我会直接在一开始就写成"#{r.begin.number_to_currency}..#{r.end.number_to_currency}"


*r不会将范围强制转换为数组吗? - max
1
@max:不,*r.bounds*(r.bounds),而不是 (*r).bounds。请记住,* 不是运算符,而是赋值左值、赋值右值、参数列表(定义和调用)和数组字面量中的语法结构。它甚至没有优先级,因为它不能在表达式“内部”进行评估。 - Amadan
谢谢,我想这更有意义。我从未考虑过你不能像操作符一样使用array.* - max
1
你可以用 [first, last] 替换 [self.begin, self.end],但不能用 [begin, end][begin, last][first, end],因为 beginend 是保留字。@max,也许你想到的是 [*1..4] #=> [1, 2, 3, 4] - Cary Swoveland
Amadan,你在第一句话中没有展示“否”的证据。毕竟这就是问题所在。另外,bounds返回一个数组。这不是违规了吗? - Cary Swoveland
显示剩余8条评论

0
Amadan的回答很好。在使用时,您只需要删除星号(*)即可,因为它是不必要的。
例如,
>“%s到%s”%(1..3).bounds.map {| x | number_to_currency(x)}
=>“$ 1.00至$ 3.00”

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接