ソフトウェア開発部 粕谷(@daiksy)です。
あるシステムの試験をしていて、少し奇妙な状況に遭遇しました。
日付を入力しそれをデータベースに登録する、という単純な処理だったのですが、
特定の日付の値が、Scalaで実装されたバリデーションは通るのに、MySQLのテーブルにinsertする際にエラーになってしまうのです。
それは、西暦1000年2月29日、という日付が入力された際に起こる現象でした。
問題となった値は、Scala上ではjava.util.Date、MySQL上ではDATEでそれぞれ扱われていました。
試しにScalaのREPLを使って以下のコードを実行してみます。
new java.text.SimpleDateFormat("yyyyMMdd") parse("10000229")
すると結果はこのようになりました。
res0: java.util.Date = Thu Feb 29 00:00:00 JST 1000
どうやら西暦1000年2月29日として、Date型で扱われるようです。
一方、MySQLの対象となるカラムに問題となる値を入れようとしたところ、
こちらはエラーになりました。
以上のことから、java.util.DateとMySQLのDATEとの間には、暦の解釈において差異があるようです。
このとき、わたしが認識していた閏年の計算アルゴリズムは下記のような具合でした。
プログラムの教科書などに記載されている閏年の計算も、上記のアルゴリズムが記載されているはずです。
このアルゴリズムに当てはめて考えてみると、西暦1000年は、
- 4で割り切れる
- 100で割り切れる
- しかし、400で割り切れない
となるので、二番目の条件にHITしますから閏年ではなく平年、ということになります。
これだけを見ると、java.util.Dateの実装に問題があるような気がします。
しかし、同様に2100年や1900年で試してみると、こちらは平年として判断されているようです。
なぜ、javaはわざわざ西暦1000年を閏年として扱うのでしょうか。
java.util.Dateの実装を見てみたところ、内部的にカレンダーの扱いが1582年を境に変わっていることがわかりました。
Date.java#Date.getCalendarSystem
GregorianCalendarクラスのjavadocを読むと、詳しい説明が書かれています。
GregorianCalendar
「グレゴリオ暦への切り換え日の前は、GregorianCalendar ではユリウス暦を実装しています。グレゴリオ暦とユリウス暦の唯一の違いはうるう年の規則です。ユリウス暦は 4 年ごとにうるう年を指定しますが、グレゴリオ暦では、400 で割り切れない世紀の初年をうるう年にしません。」
なるほど。わたしが当初認識していた閏年の計算は、グレゴリオ暦に基づくアルゴリズムであって、
javaでは西暦1582年以前はこのグレゴリオ暦ではなくユリウス暦で閏年を判定しているようです。
ちなみに余談ですが、wikipediaで西暦1000年を見てみると、「西暦(ユリウス暦)による、閏年」とありました。
wikipedia:西暦1000年
MySQLの暦の扱いは、ドキュメントで”proleptic Gregorian calendar”を使用していると定義されています。
これは「先発グレゴリオ暦」といって、1582年以前にもグレゴリオ暦を適用する、という考え方のようです。
PostgreSQLも同様の方式を採用しているようです。
MySQL: MySQL が使用するカレンダーは?
PostgreSQL: 単位の歴史
wikipedia:先発グレゴリオ暦
わたしはこれまで、日付の扱いはすべてのコンピュータシステムで同じであるはず、と思い込んでいました。
しかし、現代の我々が扱っている暦が、実際に適用されていなかった時代の日付の扱いについては、
システムによっていろいろな考え方でそれぞれの仕様が決められているようです。
西暦1000年は閏年かそうじゃないのか? 結論はこうなりました。
ちなみに、java.util.GregorianCalendar には setGregorianChange というメソッドがあり、これに値をセットすることで暦の切替日を変更できるようです。
GregorianCalendar.setGregorianChange