`significant: true` で非有限数を渡すと `FloatDomainError` が発生する問題を修正

rails/rails

RoundingHelper#digit_count に1行のガードを追加することで、significant: true オプション使用時に Infinity / NaN を渡すと発生していた FloatDomainError を修正しました。非 significant パスがすでに "Inf" / "-Inf" / "NaN" を返していた一方、significant: true パスのみクラッシュしていたという一貫性の欠如が解消されます。

背景

significant オプションの有無によって、同じ入力に対する挙動が矛盾していました。number_to_rounded は非有限数を "Inf" / "NaN" として整形するパスを既に持っており、significant: true なしでは正常に動作します。しかし significant: true を加えた途端にクラッシュします。

ActiveSupport::NumberHelper.number_to_rounded(Float::INFINITY, precision: 3)
# => "Inf"  ← 正常

ActiveSupport::NumberHelper.number_to_rounded(Float::INFINITY, precision: 3, significant: true)
# => FloatDomainError: Infinity  ← クラッシュ

この問題は Float 除算のゼロ割りや ** のオーバーフロー、外部から取り込んだ "Infinity" 文字列のパースなど、計算結果が Infinity になり得る場面でフォーマッタに渡されたときに顕在化します。また RoundingHelper を経由するすべてのヘルパー — number_to_percentagenumber_to_currencynumber_to_delimited — にも同じクラッシュが波及します。

2016年にも同様の問題を修正しようとした #25946 が存在しますが、number_to_human での "Inf" 出力が「人間に読みやすいとは言えない」という指摘を受けて2019年にクローズされました。今回のPRは RoundingHelper パスのみを対象とし、同一ヘルパー内で両パスの挙動を一致させることに絞っています。

技術的な変更

クラッシュの根本原因は RoundingHelper#digit_count 内の Math.log10 に非有限数が渡されることです。significant: true かつ precision > 0 のとき、absolute_precisiondigit_count を呼び出し、Math.log10(Float::INFINITY)Infinity を返し、その .floorFloatDomainError を投げます。

変更前:

def digit_count(number)
  return 1 if number.zero?
  (Math.log10(number.abs) + 1).floor
end

変更後:

def digit_count(number)
  return 1 if number.zero?
  return 1 unless number.respond_to?(:finite?) && number.finite?
  (Math.log10(number.abs) + 1).floor
end

追加された1行で非有限数の場合に 1 を早期リターンします。戻り値が 1 であることには意味があります。absolute_precisionprecision - digit_count + 1 を計算するため digit_count1 を返すと precision - 1 になり、その後の BigDecimal("Infinity").round(n) はどんな n でも Infinity をそのまま返します。最終的に NumberToRoundedConverter#convertrounded_number.finite? 分岐が "%f" % rounded_number 経由で "Inf" 等にフォーマットします。

テストでは3つのヘルパーインターフェース(インスタンスメソッド、クラスメソッド、ActiveSupport::NumberHelper モジュール直接呼び出し)すべてに対して Inf-InfNaN の各ケースを検証する test_to_rounded_with_significant_true_and_non_finite_value が追加されました。

設計判断

非有限数を digit_count の入口でガードするという最小限の修正が選ばれました。修正箇所を digit_count の1箇所に限定することで、number_to_roundednumber_to_percentagenumber_to_currencynumber_to_delimited の4つのヘルパーをまとめて修正できます。

respond_to?(:finite?) を用いているのは、Float だけでなく BigDecimal のような finite? を持つ数値型にも対応するためです。finite? を持たない数値型(Integer など)は事実上有限であるためガードをスキップし、既存の Math.log10 パスに進みます。

number_to_humannumber_to_human_size が今回の修正スコープから外れている点も注目に値します。これらは RoundingHelper とは別の calculate_exponent / exponent メソッド内で独自に Math.log10 / Math.log を呼び出しており、修正には別途対応が必要です。また number_to_human での "Inf" 出力の妥当性については過去のPRで議論があることから、意図的に分離されています。

まとめ

RoundingHelper#digit_count への1行追加により、significant: true パスと非 significant パスの挙動が統一されました。変更は既存の true / false 動作に影響を与えず、非有限数が渡された場合に限って従来クラッシュしていたパスが "Inf" / "-Inf" / "NaN" を返すようになります。設計意図として rounded_number.finite? 分岐がコンバーター内に既にあったことを踏まえると、digit_count にガードが欠けていたのはオリジナル実装時の見落としであり、今回の修正でその完成を見たといえます。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
fe875f70

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術的な変更・設計判断(各論)、まとめ(結論)の3部構成が明確に適用されており、理想的な記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(```言語:ファイルパス)とGitHubのPRリンク記法([#番号](URL))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

FloatDomainErrorやRoundingHelperなど、専門的なトピックを扱っており、過度な説明がなく、対象読者であるエンジニアに適した内容です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションが総論→各論で構成され、各段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則が守られており、非常に高い可読性を実現しています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内のコードブロック(変更前・変更後)は、PR情報およびDiffの内容と完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「FloatDomainError」「significant」「finite?」などの技術用語が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

クラッシュの原因(Math.log10にInfinityを渡すこと)と修正ロジック(digit_countの戻り値が1になることの影響)に関する説明は、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張(過去のPR#25946の存在、影響範囲、修正スコープなど)は、提供されたPRのDescriptionで裏付けられており、ハルシネーションは検出されませんでした。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#57451, #25946)やメソッド名(digit_count)などの固有名詞はすべて正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事のタイトルはPRの主題「significantモードにおける非有限数の一貫したフォーマット」を的確に反映しており、内容との整合性が取れています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

PR情報に基づかない外部知識(バージョン情報、リリース予定など)の追記はなく、提供された情報源に忠実です。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「2016年」「2019年にクローズ」「すでに...返していた」などの時間表現は、PR Descriptionの記述と正確に一致しています。