Rubyの範囲演算子は降順のイテレートには使えない

かなりハマったのでメモです。

範囲演算子

Rubyには.....の2種類の範囲演算子があります。実態はRangeクラスで、Range#eachメソッドでfor文のように使うイディオムがあります。

irb(main):005:0> (0..2).each { |i| p i }
0
1
2
=> 0..2
irb(main):006:0> (0...2).each { |i| p i }
0
1
=> 0...2

余談ですが、0から開始ならInteger#timesも使えます。

irb(main):007:0> 3.times { |i| p i }
0
1
2
=> 3

降順のRange

先日、(4..0)といった事をしてしまいました。ぱっと見できそうな感じなのですが、これだと全くイテレートされません。

irb(main):008:0> (4..0).each { |i| p i }
=> 4..0

理由は、Rangeのイテレートが、最初の値についてsuccを適用していって最後の値を超えたら*1終わり、という仕様だからです。上記コードの場合は、(4..0).eachの最初の周で、4.succつまり5が最後の0を越しているため、一周もしないで終わってしまう、というわけです。

ではどうするか?

Arrayに変換してArray#reverseするのは明らかにアレなので*2、何か無いか探した所、Integer#downtoを使うのが良さげです。

irb(main):021:0> 2.downto(0) { |i| p i }
2
1
0
=> 2

ちなみに、また余談ですが、Numberの場合はNumber#stepが使えそうです。

irb(main):023:0> 2.2.step(0, -1) { |i| p i }
2.2
1.2000000000000002
0.20000000000000018
=> 2.2

あれ、2.2-1が1.1にならない…何故?浮動小数点の誤差系の問題なんだろうけど、はて。
…まぁ本題ではないので、これについてはまた今度調べるとしましょう。

結論

Integer#downtoを使おう。

*1:ソース読んでないので憶測ですが、挙動からすると、内部では比較演算子が使われているのだと思います。

*2:一応 (0..4).to_a.reverse みたいに出来ますが。