Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Ruby 3.4: Rangeがイテレート不可の場合にRange#sizeがエラーにならない問題を修正(翻訳)

概要

元サイトの許諾を得て翻訳・公開いたします。

日本語タイトルは内容に即したものにしました。
読みやすさのためコードブロックの表記を訳文で変更しています。

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またはRationalRangeは、Range#sizeIntegerを返してしまいます。さらに、(..終了値)のようなbeginlessの場合は、Range#sizeInfinityを返してしまいます。

(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)

関連記事

Rails 7.1: Object#inでbeginless/endlessの日付rangeがサポートされる(翻訳)

Rails 7.1: 自動生成されるインデックス名の長さが上限を超えないようになった(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。