`number_to_delimited` が非有限浮動小数点数を破壊する不具合を修正
パフォーマンス改善として導入された高速パスが Float::INFINITY を "In,fin,ity" に変換してしまうリグレッションを、parts メソッドへの1行のガード節追加で修正しました。
背景
number_to_delimited が非有限浮動小数点数を正しく扱えなくなったのは、パフォーマンス目的で導入された2つのコミットが引き金でした。コミット 2d485aecf5 (CVE-2026-33169対応) で手動スライスによる高速パスが追加され、続くコミット 33fbedb1b1 でこの高速パスをデフォルトにするため delimiter_pattern のデフォルト値が nil に変更されました。
これらの変更以前は、正規表現 gsub!(/(\d)(?=(\d\d\d)+(?!\d))/) を使った実装が偶然にも非有限浮動小数点数を正しく処理していました。Float::INFINITY.to_s は "Infinity" という文字列になりますが、この文字列には数字(\d)が含まれないため正規表現がマッチせず、文字列がそのまま返されていたのです。高速パスは "Infinity" を8文字の「数字列」として扱い、右から3文字ごとにカンマを挿入するため、"In,fin,ity" という破損した文字列が生成されてしまいました。
Float::NAN が同じリグレッションを免れたのも意図的な設計ではなく、"NaN" がちょうど3文字であったため offset = 0 となり、1回のイテレーションで同じ文字列が返る偶然によるものでした。この問題は、同じ非有限浮動小数点数に関する #57451(significant: true 時に FloatDomainError が発生する問題)と同系統ですが、別のコンバータ・別のコードパスに起因する独立した不具合です。
技術的な変更
NumberToDelimitedConverter の parts メソッドの先頭に、非有限浮動小数点数を早期リターンするガード節を1行追加しました。
変更前:
def parts
left, right = number.to_s.split(".")
if delimiter_pattern
left.gsub!(delimiter_pattern) do |digit_to_delimit|
変更後:
def parts
return [number.to_s] if number.respond_to?(:finite?) && !number.finite?
left, right = number.to_s.split(".")
if delimiter_pattern
left.gsub!(delimiter_pattern) do |digit_to_delimit|
number.respond_to?(:finite?) という条件式によって、Float 以外の型(Integer など finite? を持たないオブジェクト)への影響を完全に排除しています。ガード節を通過した場合は [number.to_s] を返し、後続の join で "Infinity"・"-Infinity"・"NaN" がそのまま出力されます。
修正後の動作は以下のとおりです:
number_to_delimited(Float::INFINITY) # => "Infinity"
number_to_delimited(-Float::INFINITY) # => "-Infinity"
number_to_delimited(Float::NAN) # => "NaN"
この挙動は number_to_phone(Float::INFINITY) が既に "Infinity" を返す挙動と一致しており、NumberHelper 全体での一貫性が保たれます。テストには3つのヘルパーアクセス経路(インスタンスメソッド、クラスメソッド、ActiveSupport::NumberHelper モジュール直接呼び出し)すべてを対象にしたケースが追加されています。
設計判断
finite? の有無を respond_to? で確認する防御的アプローチが採用されました。
Float::INFINITY を渡したときに例外を発生させる(フェイルファスト)のではなく、文字列表現をそのまま返す方針が選択されています。これは number_to_phone を始めとする他のヘルパーの既存の挙動と整合する選択であり、非有限値を "Infinity" として出力する一貫したAPIを NumberHelper 全体で提供する意図が読み取れます。
ガード節の配置を convert メソッドではなく parts メソッドの先頭にした点も重要です。parts は高速パスと正規表現パスの両方が通る共通の入口であり、ここで短絡することで将来の内部実装変更に対しても保護が機能し続けます。
まとめ
本PRは、パフォーマンス改善コミットが偶発的な正しさに依存していた箇所を顕在化させたリグレッションへの修正です。respond_to?(:finite?) を用いた1行のガード節という最小限の変更で、既存の動作に影響を与えずに非有限浮動小数点数の取り扱いを明示的・意図的に正しく定義しています。