Ruby 3.4: Rangeがイテレート不可の場合にRange#sizeがエラーにならない問題を修正(翻訳)
RubyのRangeは値の範囲を表すオブジェクトで、開始と終了という2つの値が定義されます。Rangeは基本的なデータ構造の一種であり、2つの値には数値やアルファベットに加えて日付も指定可能です。Rangeは、値のシーケンスを表すのにも、値の範囲を表すのにも使えます。
範囲は、以下のように2通りの方法で指定できます。
- 1: 
..を用いる"inclusive"な方法 
(開始値..終了値)とすると、終了値が範囲に含まれます。
inclusive_range = (1..5)
#=> [1, 2, 3, 4, 5]
- 2: 
...を用いる"exclusive"な方法 
(開始値...終了値)とすると、終了値は範囲に含まれません。
# `...`を使うと1, 2, 3, 4は含まれるが5は含まれない
exclusive_range = (1...5)
#=> [1, 2, 3, 4]
🔗 RubyのRange#sizeメソッド
Range#sizeメソッドは、その範囲に含まれる要素の個数を返します。開始値と終了値はどちらもNumericでなければならず、それ以外の場合はnilを返します。
(10..20).size   #=> 11
(10...20).size  #=> 10
('a'..'z').size #=> nil
🔗 修正前
Rangeの開始値がFloatまたはRationalの場合、「要素の個数」という概念は無意味です。そのようなRangeは「2の次は3、3の次は4...」のようなイテレートができないので、配列のような「要素」は存在しようがないことがわかります。
それにもかかわらず、開始値がFloatまたはRationalのRangeは、Range#sizeでIntegerを返してしまいます。さらに、(..終了値)のようなbeginlessの場合は、Range#sizeでInfinityを返してしまいます。
(0.51..5).size          #=> 5
(0.51..5.quo(2)).size   #=> 2
(5.quo(2)..10).size     #=> 8
(..1).size              #=> Infinity
どうやら、Rangeの開始値と終了値を両方ともNumeric#roundで直近の整数値に丸めてから、Range#sizeが計算されていたようです。
🔗 修正後
Ruby 3.4からは、Rangeがイテレート不可の場合はRange#sizeでTypeErrorを発生するよう修正されました(#8663)。
(0.51..5).size          #=> can't iterate from Float (TypeError)
(0.51..5.quo(2)).size   #=> can't iterate from Float (TypeError)
(5.quo(2)..10).size     #=> can't iterate from Rational (TypeError)
(..1).size              #=> can't iterate from NilClass (TypeError)
      
概要
元サイトの許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。
読みやすさのためコードブロックの表記を訳文で変更しています。