`number_to_phone`が複数文字デリミタを先頭から正しく除去するよう修正

rails/rails

7桁以下の電話番号に複数文字の:delimiterを指定すると、先頭に不正な断片が残るバグが修正されました。slice!(0, 1) という1文字固定の除去コードを slice!(0, delimiter.length) に変更するだけの1行修正です。

背景

number_to_phone:delimiter オプションに任意の文字列を受け付けますが、「先頭のデリミタを除去する」処理が1文字固定になっていたため、複数文字のデリミタで誤動作していました。具体的には、-, のようなデリミタを7桁の電話番号(エリアコードなし)に適用すると、以下のような誤った出力が生成されていました。

number_to_phone(5551234, delimiter: " - ")
# => "- 555 - 1234"   (期待値: "555 - 1234")

number_to_phone(5551234, delimiter: ", ")
# => " 555, 1234"     (期待値: "555, 1234")

このバグは2011年の #3314 で導入された修正に由来します。#3314 はエリアコードなしの場合に先頭デリミタが残る問題を修正したPRでしたが、その実装が slice!(0, 1) という1文字固定の除去に留まっていたため、複数文字デリミタのケースが見落とされていました。テストスイートが1文字のデリミタと10桁番号のみを対象にしていたことで、13年間この問題が眠り続けていました。

技術的な変更

convert_without_area_code メソッド内の1行を修正することで、デリミタ長に応じた正確な除去が実現されています。

変更前:

number.slice!(0, 1) if start_with_delimiter?(number)

変更後:

number.slice!(0, delimiter.length) if start_with_delimiter?(number)

デリミタが先頭に付くのは、正規表現パターン /(\d{0,3})(\d{3})(\d{4})$/ の第1キャプチャグループ \d{0,3} が空(7桁以下でエリアコードなし)になるケースのみです。この場合、gsub後の文字列は "\1#{delimiter}\2#{delimiter}\3" の展開により - 555 - 1234 のようにデリミタが先頭に現れます。start_with_delimiter? がその状態を検出してから slice! で除去する仕組みですが、除去サイズが1文字固定だったため複数文字デリミタに対応できていませんでした。

start_with_delimiter?delimiter.present? を保証しているため、delimiter.length は常に1以上になります。また、String#slice! は文字単位で動作するため、マルチバイト文字のデリミタ(例: "—" U+2014 EMダッシュ)も正しく処理されます。追加されたテストケースは以下の3つです。

assert_equal("555 - 1234",       number_helper.number_to_phone(5551234,    delimiter: " - "))
assert_equal("800 - 555 - 1212", number_helper.number_to_phone(8005551212, delimiter: " - "))
assert_equal("555—1234",         number_helper.number_to_phone(5551234,    delimiter: "—"))

10桁番号のテストケース(8005551212 + -)も追加されており、エリアコードあり・なし両方でデリミタが正常に機能することが確認されています。

設計判断

修正範囲を最小限に絞る方針 が採用されており、コードパスの分岐追加や正規表現の変更は一切行っていません。

変更箇所は slice!(0, 1) から slice!(0, delimiter.length) への1文字の変更のみです。start_with_delimiter? による条件分岐や gsub のパターンはそのまま維持されており、既存の動作を保証するすべての条件(10桁番号、エリアコードあり、カントリーコード、内線番号、空デリミタ)は影響を受けません。PR本文によると、変更後も number_helper_test.rb の全22テスト・819アサーションが通過しています。

マルチバイト対応が自動的に得られる点も設計上の利点です。String#slice! が文字単位で動作するRubyの仕様により、追加の特殊処理なしにUnicode文字のデリミタを正しく扱えます。

まとめ

2011年の修正で誤って残された 1 というマジックナンバーを delimiter.length に置き換えるだけで、ドキュメントに記載された「任意の文字列を:delimiterに指定可能」という仕様が初めて完全に実現されました。複数文字デリミタやマルチバイトデリミタを利用していたアプリケーションは、このバグフィックスにより期待通りの出力を得られるようになります。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
49d436b5

この記事は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番号リンク([#123](URL))など、カスタムMarkdown構文がすべて正しく使用されています。

対象読者への適合性 ✓ PASS

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

Railsヘルパーの内部実装という専門的なトピックを、前提知識を持つエンジニア向けに適切な粒度で解説しており、対象読者に完全に適合しています。

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

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

各セクション、各パラグラフが「総論→各論」の構造で書かれ、すべての段落がトピックセンテンスで始まるなど、パラグラフ・ライティングの原則を完全に満たしています。

Diff内容との照合 ✓ PASS

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

記事に引用されているコード変更(変更前・変更後)および追加されたテストコードは、提供されたDiff情報と完全に一致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「slice!」、「キャプチャグループ」、「マルチバイト文字」などの技術用語が、PRの文脈に沿って正確かつ適切に使用されています。

説明の技術的正確性 ✓ PASS

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

バグの発生メカニズムや修正による影響範囲の説明は、PR情報に基づいており、技術的に正確かつ論理的です。

事実の突合 ✓ PASS

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

記事内のすべての主張(バグの歴史的経緯、テストが通過したことなど)はPRのDescriptionやDiffで裏付けられており、ハルシ네ーションは一切見られません。

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

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

PR番号(#57375, #3314)、テストのアサーション数(819)、マジックナンバー(1)など、記事中の数値や固有名詞はすべて正確です。

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

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

記事のタイトルはPRの主題を的確に要約しており、内容との整合性が完全に取れています。

外部知識の正確性 ✓ PASS

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

記事の内容は提供されたPR情報に限定されており、バージョンサポート状況などのPR外の知識を追記するようなハルシネーションはありません。

時間表現の正確性 ✓ PASS

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

「2011年」「13年間」といった時間に関する表現は、PRの情報と整合性が取れており、正確です。