閏日を処理する過程で不具合を生み出しかねない実装に思い当たったので、思考整理を兼ねてメモしておきます。
きっかけ
何気なくWikipediaを見ていて下記の記述を見つけ「なるほどなぁ」と思ったことをtwitterに投稿したところ、「システム化するときに頭が痛い」というreplyをいただきました。(仕様と処理が複雑になり、コード等の保守性が下がる。)
2月29日生まれの者の誕生日は閏年に限り到来し、平年に誕生日は存在しない。日本の法律では、誕生日を基準とした行政手続に限り「みなし誕生日」を2月28日としている。
その瞬間は「確かに」とだけreplyしようとしたのですが、よく考えてみると、当てはまる事例が他にもあるのではと思いました。
不具合の例
例えば、記事のタイトルにもしている『「”1年前の今日”に投入されたデータを処理」する夜間バッチ』があったとします。この夜間バッチは毎日1:00になるとcronで1度だけキックされます。仕様書通り素直にコーディングすると、下記のSQLが製造されます。
SELECT date_sub(current_date(), INTERVAL 1 year);
更に、バッチの処理対象を取得する処理において、上記SQLは下記のような使われ方をされることでしょう。(createdはデータが投入された日付を表現するとします。)
SELECT * FROM HOGE_DATA WHERE created = date_sub(current_date(), INTERVAL 1 year);
このような実装をした場合、閏日(2/29)に投入されたデータの処理が抜け落ちます。
どう整理すべきか
いろいろと解法はあるように思いますが、私はまず仕様がまずいと考えます。上流工程の段階で、顧客とは『「”1年前の今日”以前に投入された、未処理のデータを処理」する夜間バッチ』と合意すべきでした。
仕様整理により、2012-02-29のデータは、2013-03-01のバッチで正常に処理対象とされます。また、元の仕様に立ち返ると、ひとつのデータが2度処理されることは有り得ないため、その要件を満たすように処理未済フラグ等のカラム(タプル)をテーブルに追加して、検索条件に付与します。但し、業務上は処理日時を各データに持たせることが多いため、フラグとして新たに定義する必要はない場面が多いと思います。(processedはデータが処理された日付を表現するとします。バッチの処理対象となったデータは必ず日付でupdateされる仕様です。)
SELECT * FROM HOGE_DATA WHERE created <= date_sub(current_date(), INTERVAL 1 year) and processed IS NULL;
indexを考慮した設計にする必要は別途ありますが。
以上、雑多な思考整理でした。日付関連の仕様を整理するときは、閏日閏年を頭の片隅に置いておいたほうがいいですね……。