タイムゾーンのよもやま話 を書いてて、日付処理についてもいろいろあるよなぁと思ったので書き出してみました。
「そんなん常識でしょ」ってのがほとんどだと思いますが、まとまってると便利かもしれないということで。
和暦
日本で日付を扱う場合は和暦を考慮する必要がある場合があります。西暦しか受け付けない!ってしたいところですが、そうもいかない場合もあると。
和暦と西暦の変換で困るのは以下のようなことでしょうか。
- 和暦と西暦の対応は変換データを持つしかないこと
- 1年は元年という特別表記となること
- 改元は年初に行われるとは限らないため、西暦から和暦に一意に変換できないこと(例 : 西暦2019年 -> 平成31年、令和元年)
- 改元がいつ行われるか、元号が何になるかは事前に予測不可能なこと
つい最近令和への改元が行われたのでみなさん騒動は記憶に新しいと思いますが、改元はいつ行われるか予想がつきません。昔のシステムでは昭和100年とかなってるものもありますし、そういう並行世界を作ってしまうのもシステムに閉じているならありかもしれませんね。
改元はいつ起こるかわからないといいつつ、そうそう頻繁にあることではありませんので、システムで定数として持っててもさほど問題はないかと思います。改元があった際には定数を追加して対応すると。平成への改元の時のように即日改元という可能性もありますが、その場合にはしばらく前の元号を使っても社会的に許されるのではないでしょうか。
元号はいつから対応しましょうか。景気よく大化から全部対応といきたいところですが、後述の通り明治4年以前はグレゴリオ暦ではないため和暦西暦の変換は一筋縄ではいきません。西暦側も1581年以前はユリウス暦ですし。現実的には明治以降のみ対応となるでしょう。
とにかく重要なのは、DB に保存する年は西暦にしておくべきことでしょうかね。タイムゾーンでは DB 保存は UTC+0 で行うのが望ましいように。
うるう年
現在日本を始めとして多くの国で採用されているグレゴリオ暦では、うるう年を以下のように定めています。
- 年が4で割り切れる場合はうるう年である
- ただし、年が100で割り切れる場合はうるう年ではない
- さらに、年が400で割り切れる場合はうるう年である
2020年はうるう年で、2100年はうるう年ではなく、2000年はうるう年です。2000年は400年に一度しかない特別なうるう年ですので、ほとんどの人は一生遭遇することはありません。そもそも1582年に制定されたグレゴリオ暦でも1600年に次いで2回目なわけです。そんな希少なうるう年に、ITシステムが急速に普及しだした1990年代からわずか数年でぶち当たってしまったのは運が良かったのか悪かったのか。
2000年問題というと、年を2桁で保持していた場合に桁あふれを起こす問題が重視されていましたが、実はうるう年問題もありました。年があふれる1月1日ばかりが注目されたのでそちらは実際には障害はあまり起きず、2月29日の方で障害がいくつも発生してしまったりもしたそうです。
今どきの主要なOSや言語、ライブラリならさすがにグレゴリオ暦のうるう年は問題なく対応されてるとは思いますが、問題はマイナーなライブラリだったり自力実装したときに、ついうっかり対応を忘れた場合でしょうかね。なんせ400年に一度しか起きない事象ですから、次は2400年になるまで発覚しないわけでして。
また、古いシステムを使ってる場合には注意が必要です。昔のパソコンでは使用してるカレンダICが1年分しか対応してなくて、そもそも4年に一度のうるう年すら手動で合わせないといけないとかありました。互換性やコストの問題で、今でもこのようなカレンダICが使用されていることもあるのではないかと思います。
来月の今日、来年の今日
3月31日の1か月後は4月30日でしょうか、5月1日でしょうか。そのシステムの仕様によるでしょうから、実装者が勝手に決めていい問題ではないですね。同様に2020年2月29日の1年後は2021年2月28日なのか3月1日なのかというのも仕様で考えなければなりません。
第n曜日
タイムゾーンが絡んでくると、月末日以外にも問題は起こりえます。
Windows Update は毎月第2火曜日に公開されることになっています。これはマイクロソフト本社があるシアトルの標準時ですから、日本時間ではだいたい翌日になります。では毎月第2水曜日はパソコンを再起動するようにスケジュールに登録しておこうと思ってもうまくいきません。
例えば2020年4月のカレンダーは以下のようになっています。
日 | 月 | 火 | 水 | 木 | 金 | 土 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 |
第2火曜日は14日ですが、第2水曜日は8日。自分のパソコン程度なら翌週にも手動で再起動すればいいだけのことですが、月次バッチ処理のスケジューリングで同じことをやってたら、下手したら大惨事ですね。
グレゴリオ暦以前
グレゴリオ暦は1582年から使用されていますから、それ以前の日付を扱う際にはどういう暦法を使うか考えなければなりません。日本でグレゴリオ暦が採用されたのは1873年ですので、それ以前の日付を扱う場合に検討が必要なのは同様です。
また、現在でもグレゴリオ暦を採用していない国や地域がありますので、それらの地域の対応をどうするかも検討事項です。
エポック秒と1970年以前、2038年以降
UNIX 系のシステムでは 1970年1月1日0時0分0秒からの経過秒数であるエポック秒を多用します。
エポック秒では当然に1970年以前を扱えませんので、その点で注意が必要です。また、エポック秒を32ビット整数で保持した場合には2038年1月19日3時14分7秒までしか扱えません。64ビット整数で保持するなどの対応が必要です。
1970年より以前の日付なんて、そんな古い日付は扱わないと思うでしょうが、例えば生年月日はどうでしょう。1970年生まれは今年50歳。まだまだたくさん世の中にいらっしゃいます。うっかりと生年月日をエポック秒で保持するように実装してしまい、テストした人が全員1970年以降生まれで気づかなくてリリースなんてことは無い話とは言えないでしょう。
祝日
1月1日元旦などの固定の祝日のみであれば話は簡単なのですが、振替休日や移動する祝日などの対応となると、ロジックで対応するのは大変です。さらに祝日は追加されることがありますので、ある年以降のみ祝日として扱うように対応しないといけませんし、逆に廃止されることもあります。特別法によりある年のみに存在する祝日もあります。
ということを考えると、祝日をロジックで判定したり、祝日データを自前で用意するのは無理があると言えるでしょう。こういうことはライブラリか API に頼るのが良いのですが、そのライブラリや API がきちんと更新されているかは確認しましょう。何年も放置されてるライブラリを使っても天皇即位や東京オリンピックの祝日には対応できません。
ライブラリを使うときの注意をもう一つ。ローカルにライブラリをインストールする際にはバージョンアップによる誤作動を防ぐためにバージョンを固定するのが一般的ですが、祝日ライブラリに関してのみはバージョンアップを行わなければなりません。この辺は運用計画の中に組み込んでおかなければなりませんね。
また、祝日は国によって違ってきます。世界中の国の祝日に対応しようとしたら、とても自力では対応できそうにありませんのでライブラリか API に頼るしかなさそうです。
n年問題
2000年問題や2038年問題が有名ですが、他にもn年問題は多数あります。これらの問題の根本原因は有限のコンピュータリソースで無限の時間に対応しようとするところにあると言えるでしょう。
年を2桁で保持して2000年問題を引き起こしたプログラマを現代のプログラマは愚かだと笑うかもしれませんが、年を4桁で保持していれば10000年問題を引き起こすことは確定です。そんな先のことは知らんと言っても、2000年問題を引き起こした人たちも、やはりそんな先のことは知らんと言っていたわけですね。
だからといって年の保持に多倍長整数を使うというのは牛刀感に溢れています。実際には、これくらい先まで対応していれば十分だろうというところで区切って有限で対応するのが落としどころではないかと思います。
テストがしにくい問題
日時処理で困るのがテストがしにくいことです。うるう年の処理が正しいかどうかを、実際にうるう年にならないとテストできないでは話になりません。
メソッドごとの単体テストであれば、メソッドでは現在日時を取得せずに呼び出し側からパラメータとして与えてもらうようにすることで対処できます。現在日時の取得はアプリケーションの一か所に集約しておいて、テスト時にはそこを代替処理に置き換えればいいわけですね。
単体テストはこれでなんとかなったとしても、システム全体のテストとしては難しく、最後にはパソコンの時計を一時的に変更してテストせざるを得ないでしょうか。