`ActiveSupport::TimeFormats` / `DateFormats` によるカスタム日付フォーマット登録の刷新

rails/rails

カスタム日付フォーマットを to_fs に追加する際に Time::DATE_FORMATSDate::DATE_FORMATS といったグローバルなHashを直接書き換える必要がなくなりました。新たに導入された ActiveSupport::TimeFormats.register / ActiveSupport::DateFormats.register を使うことで、ActiveSupport 管理下の名前空間に閉じた形でフォーマットを登録できます。

背景

この変更は、グローバル定数のフリーズ問題をめぐる一連の議論から生まれました。#57332Date::DATE_FORMATS に誤って freeze が適用されていた問題を修正したものですが、そもそも「ユーザーがグローバル定数を直接変更する」設計そのものへの懸念が @byroot によるコメントで指摘されました。続いて #57341 がミュータブルな定数の変更を期待するテストを追加し、この設計が変更されることへの地ならしを行っています。

Time::DATE_FORMATSDate::DATE_FORMATS はグローバルなHashであるため、他のライブラリが同じキーを登録すると衝突が起きる可能性がありました。また、直接書き換えという操作自体がカプセル化を損なっており、テストでは ensure ブロックで後片付けが必要になるなど、副作用のある API 設計でした。

技術的な変更

ActiveSupport::TimeFormatsActiveSupport::DateFormats という2つのモジュールが新たに追加されました。それぞれ activesupport/lib/active_support/time_formats.rbactivesupport/lib/active_support/date_formats.rb として実装され、active_support.rbautoload 宣言に追加されています。

各モジュールの実装は共通のパターンに従っています。

module ActiveSupport
  module TimeFormats
    @list = Time::DATE_FORMATS.dup.freeze
    @deprecated_list = Time::DATE_FORMATS
    Time.deprecate_constant :DATE_FORMATS

    # :nodoc:
    def self.lookup(format)
      @list[format] || @deprecated_list[format]
    end

    def self.register(name, format)
      @list = @list.merge(name => format).freeze
    end
  end
end

モジュール初期化時に既存の Time::DATE_FORMATSdup.freeze してコピーし、元の定数を deprecated_constant として宣言します。lookup メソッドは新しい @list を優先しつつ、旧定数への参照 @deprecated_list にもフォールバックすることで後方互換性を保っています。

register メソッドは @list.merge(name => format).freeze というイミュータブルな更新パターンを採用しています。毎回新しいfreezeされたHashを生成するため、登録後の @list は常にfreezeされた状態が保たれます。

Date#to_fsTime#to_fsDateTime#to_fsActiveSupport::TimeWithZone#to_fs のすべてで、ルックアップが DATE_FORMATS[format] から ActiveSupport::TimeFormats.lookup(format) / ActiveSupport::DateFormats.lookup(format) に切り替わっています。

# 変更前
def to_fs(format = :default)
  if formatter = DATE_FORMATS[format]
    formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
  else
    to_s
  end
end

# 変更後
def to_fs(format = :default)
  if formatter = ::ActiveSupport::TimeFormats.lookup(format)
    formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
  else
    to_s
  end
end

テストコードも新しい API に対応し、グローバルな定数を書き換えて ensure で戻す従来のパターンから、ActiveSupport::TimeFormats.stub(:lookup, ...) を使ったスタブパターンへと刷新されました。これにより、テストの副作用が完全に排除されています。

設計判断

後方互換性と段階的移行 を両立させる設計が採用されています。@deprecated_list として旧定数への参照を保持し続けることで、既存コードは Time::DATE_FORMATS[:custom] = ... という書き方を即座に壊されることなく、非推奨警告を経て次バージョンでの削除に向けた猶予期間が設けられています。

register 後の即時フリーズ という設計も重要な判断です。PR の説明によれば、register が一度でも呼ばれた時点で @list がフリーズされます。これは「新しい API にオプトインしたユーザーは、グローバル定数を直接変更する旧来の方法を混在させるべきでない」という意図の表明です。

ルックアップのロジックに @list[format] || @deprecated_list[format] というフォールバック順序が設けられている点も注目に値します。register で登録されたフォーマットが旧定数の同名エントリより優先されるため、移行期間中に両方のフォーマットが登録されていても新しい API が勝ちます。

まとめ

ActiveSupport::TimeFormats / DateFormats の導入により、カスタム日付フォーマットの登録がグローバルなHashの直接変更からカプセル化された API 呼び出しへと移行しました。旧定数の非推奨化と lookup のフォールバック設計によって段階的な移行を可能にしつつ、イミュータブルな @list 管理によりスレッドセーフ性も向上しています。

記事メタデータ

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

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

品質レビュー結果

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

Review Criteria:

記事構成 ✓ PASS

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

リード文(総論)→背景・技術詳細・設計判断(各論)→まとめ(結論)という理想的な構成が実現できています。各セクションの役割が明確で、非常に読みやすいです。

カスタムMarkdown構文 ✓ PASS

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

ファイル名付きのシンタックスハイライトや、PR番号・コメントへのリンク記法が正しく使用されています。

対象読者への適合性 ✓ PASS

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

グローバル定数の問題点、カプセル化、イミュータブルな更新など、Rails開発者を対象とした専門的な内容が、過不足なく説明されています。

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

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

各セクション、各パラグラフが「総論→各論」の構造で書かれており、トピックセンテンスが先頭に置かれているため、非常に理解しやすい文章になっています。

Diff内容との照合 ✓ PASS

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

記事内で引用されているコードは、Diffの内容と正確に一致しています。本質的な部分を適切に抜粋・比較しており、変更点が明確に伝わります。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

「カプセル化」「イミュータブル」「後方互換性」などの技術用語が、文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

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

「lookupメソッドのフォールバック処理」「registerメソッドのイミュータブルな更新」など、コードに基づいた技術的な説明が論理的かつ正確です。

事実の突合 ✓ PASS

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

記事内の主張はすべてPRのDescriptionやDiffの内容で裏付けられており、ハルシネーション(創作)は一切見られません。特に設計判断に関する説明はPRの意図を正確に汲み取っています。

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

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

PR番号(#57345)や関連PR番号(#57332, #57341)が正確に記載・リンクされています。

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

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

記事のタイトルはPRの主題「Introduce ActiveSupport::TimeFormats and ActiveSupport::DateFormats」を的確に要約しており、内容との乖離がありません。

外部知識の正確性 ✓ PASS

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

PR情報に含まれない外部知識(バージョンのサポート状況など)の持ち込みはなく、提供された情報源に忠実です。

時間表現の正確性 ✓ PASS

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

「旧定数の非推奨化」「次バージョンでの削除」といった時間的な前後関係が、PRの記述に沿って正確に表現されています。