Date.new の第四引数

昨日、Rails の日付・時刻入力周りを見ていて初めて知ったんだけど、Date.new は年・月・日のほかに省略可能な第四引数として「グレゴリオ歴の開始日」を「ユリウス日」で指定できるらしい。何も指定しなかった場合は、デフォルトの Date::ITALY の値が使われる。

この辺の扱いはどうなっているんだろう?とRails 2.1.1のソースを見てみると、mass assignmentを使った場合のdateカラムに対応する属性値の作成は

# valuesは普通 [年, 月, 日] の配列
begin
  Date.new(*values)
rescue ArgumentError => ex
 # Timeを使って作成し直す
 instantiate_time_object(name, values).to_date
end

のようになっていて、引数の数を制限していない。values に入る値はユーザがリクエストパラメータをいじることで結構好き勝手に設定可能であり、第四引数の「グレゴリオ歴の開始日」も自由にセットできるようだ。

これは、日付の範囲をチェックしていないか、チェックしていても1900年とか2100年という年を受け入れるアプリケーションで問題になる可能性がある。

例として、ユーザが画面入力として「年=1900, 月=2, 日=29」を送ってきた場合を考える。第四引数が無いと、グレゴリオ歴の開始日は Date::ITALY の値、1582年10月15日が使われる。1900年はこの日よりも後なので、引数のチェックにはグレゴリオ歴が使用される。1900年はグレゴリオ歴では平年なので、2月29日は不正な日付であり、ArgumentError が送出される。この後、デフォルトの動作では、Timeクラスを使って3月1日に修正されたDateオブジェクトがモデルに設定されることになり、問題はない。

ところが、ユーザがリクエストパラメータをいじって「グレゴリオ歴の開始日」を追加し、「年=1900, 月=2, 日=29, グレゴリオ歴の開始日=2451545」(2451545は2000年1月1日に対応するユリウス日)を送ってきたとする。こうすると1900年はグレゴリオ歴の開始日より前になり、引数のチェックにはユリウス歴が使用される。1900年はユリウス歴ではうるう年なので、2月29日は正しい日付であり、「ユリウス歴1900年2月29日」というDateオブジェクトがモデルに設定される。もちろん validates_presence_of のチェックも普通に通る。

データベースの種類によって異なるのかもしれないけど、SQLite3については、この1900年2月29日という暦によって正当性が微妙な日付も、dateカラムにそのまま保持される。しかし、この日付をデータベースから取ってきたとき、Railsは固定でグレゴリオ歴として扱うので、解釈不能な日付として値が nil になってしまう。

こうして色々な条件が積もり積もると、validates_presence_of があるから必ず値が入っている! という前提のアプリケーションでは予期せぬ nil で NoMethodError → システムエラーが発動するとかなるわけだ。

グレゴリオ歴固定と割り切るなら、最初のコードは

  Date.new(*(values[0..2]))

としたほうが良さそうな気がしないでもないが、影響とかよくわからないからなあ。

……以上、重箱の隅でした。