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]))
としたほうが良さそうな気がしないでもないが、影響とかよくわからないからなあ。
……以上、重箱の隅でした。