🌏

タイムゾーン呪いの書 (実装編)

2021/07/02に公開
2

「タイムゾーン呪いの書」は、もともと 2018年に Qiita に投稿した記事でしたが、大幅な改訂を 2021年におこない、同時にこちらの Zenn に引っ越してきました。この改訂で記事全体が長大になったので、「知識編」・「実装編」・「Java 編」と記事を分けることにしました。

この「実装編」は、導入にあたる「知識編」の続きとなる第二部です。おもに Software Design 誌の 2018年 12月号に寄稿した内容をベースにしていますが、修正した内容もかなりあります。本記事全体を通して「知識編」を読んでいることを前提にしているので、ご注意ください。旧 Qiita 版にあった Java 特有の内容は、第三部にあたる「Java 編」にあります。

はじめに

先の「知識編」では、この時刻とタイムゾーンという厄介な概念について一般的な知識を紹介してきました。さて、ではこの知識を具体的に実装に落とし込むとき、何にどう気をつけたらいいでしょうか。

これまでも Java の JSR 310: Date and Time API を参照しましたが、この JSR 310 については「単なる日付・時刻をあつかうだけなのに複雑すぎる!」という声をよく耳にします。が、ここまで書いてきたように、時刻やタイムゾーンという概念は、そもそも超複雑なんです。 JSR 310 はこの「超複雑な時刻やタイムゾーンという概念」をかなり忠実にモデル化しているので、実は Java にかぎらず実装の一般論を検討するのにもいいモデルケースになると思います。

時刻やタイムゾーンをあつかうときに、「たいていこうしておけば大丈夫」といえるような銀の弾丸は、残念ながらありません。「日付・時刻をあつかうときのベスト・プラクティス」のような解説記事もときどきありますが、日本国外や例外的な状況ではまりがちな落とし穴を回避できない内容になっていることも多いです。

ベスト・プラクティスで思考停止せず、時刻とタイムゾーンの概念を理解し、要件を吟味し、その都度いいやりかたを要件に合わせて考え、選ぶ必要があります。 [1] この「実装編」では言語やソフトウェア特有の話にはなるべく踏み入らずに [2] 以下の三つの視点で、手法の選びかたや、一般的な考えかたを紹介します。

  • 内部データ表現
  • データの出力と永続化
  • データの入力

内部データ表現

最初に検討するのは、時刻データをソフトウェアの中でどういう形式で保持し、持ち回るか、つまり時刻の内部データ表現をどうするか、です。

「ベスト・プラクティスはない」とは書きましたが、それは単体では不十分だというだけのことで、「少なくともこれだけは守っとけ」といえる大原則はあります。その大原則を紹介したあと、時刻のあつかいかたを要件ごとに検討するための、以下の三つの基準をくわしく検討します。

  • うるう秒をあつかう必要があるか?
  • 暦の計算、タイムゾーンの切り替わりをまたぐ計算をするか?
  • 確定した過去の時刻か? 未来の予定の時刻か?

大原則

タイムゾーンが未確定なままの「年・月・日・時・分・秒」 [3] を、そのまま保持し続けない、そのまま持ち回らない、というのが、その大原則です。

API レスポンスやユーザー入力のような外部データに、タイムゾーンが不明だったりあいまいだったりする「年・月・日・時・分・秒」が入っていることは、残念ながらよくあります。それは外部要件で決まっていたりして、どうしようもないこともあります。ソフトウェア・エンジニアとして気をつけるのは、その時刻を、タイムゾーンが未確定のまま保持し続けない、持ち回らないことです。その時刻がどのタイムゾーンのものか、できるだけ早期に特定・確定し、時刻とタイムゾーンをセットで保持するようにしましょう。

ほとんどのソフトウェアは、複数の人の手でメンテナンスします。タイムゾーンが未確定のあいまいな情報をそのまま保持していると、認識の齟齬から、容易にバグを埋め込んでしまいます。特に、公開メソッドの引数や返り値としてタイムゾーン未確定な時刻を受け渡していたら赤信号です。

OS や実行環境のタイムゾーン設定 (環境変数 TZ など) があるからコード中で明示的にタイムゾーンを確定しなくても大丈夫、というのは、典型的なアンチ・パターンです。その理由の一つは、開発時・テスト時・運用時の違いでバグを見逃すことがある、という一般論です。さらにいえば、多くの「OS や実行環境のタイムゾーン設定」は地域ベースです。つまり OS や実行環境の設定に頼ると、必要ない場合でもタイムゾーンの呪いが自然についてきます。

地域ベースのタイムゾーンだけではなくオフセットまで確定して「Unix time に変換可能」な形式にすれば、または UTC に変換して Unix time そのものにできれば、タイムゾーンの呪いはかなり軽減できるでしょう。 Unix time に変換可能ということは、世界共通の時間軸上の位置が確定しているということで、その時刻データをあいまいさのない絶対時刻にできているということです。

とはいえこれから検討してくように、「Unix time に変換可能」な形式は、常にベストとはかぎりません。「時刻はとりあえず UTC に変換しておけ」とはよくいわれますが、それは間違ったベスト・プラクティスです。

世の中にさまざまな要件があるなかで、ソフトウェア・エンジニアは時刻をどうあつかえばいいのか、これから検討していきましょう。

うるう秒をあつかう必要があるか?

最初に検討するのはうるう秒です。厳密な時刻が要求される分野 [4] では、うるう秒も厳密にあつかう必要があるかもしれません。「知識編」で解説した Unix time の定義から、この場合 Unix time を使う選択肢は消滅します。

しかし、うるう秒をあつかうのは茨の道です。実装の複雑さも、実装とテストにかかる時間も、バグの入り込みやすさも、すべてが大幅に上がることを覚悟しましょう。 [5] うるう秒がビジネスを左右するほどの重要な要件でないかぎりは、うるう秒をあつかう仕様にすること自体を、できるだけ避けることをおすすめします。たとえば、一般ユーザー向けの Web サービスでうるう秒が必要なケースはほぼないと思います。うるう秒は、上手にごまかすのが第一選択肢です。

一秒の長さが厳密でなくともよければ、「知識編」で紹介した Java タイム・スケール (UTC-SLS) や、クラウドサービスの Leap Smear がそのまま使えるかもしれません。ほとんどの場合はそうだと思いますし、それが一番簡単です。または、ミリ秒など秒未満の時間を考える必要がなければ「59分 59秒」に 2秒かける選択肢もあるかもしれません。負のうるう秒の場合は単に「59分 59秒」が消滅します。逆進さえなければいい、というケースでは、これも方法の一つかもしれません。必要なものが「経過時間」であって時刻ではない場合、ほとんどの言語や標準ライブラリに、経過時間をはかるための API が時刻関係とは別に用意されていることを覚えておきましょう。 [6]

うるう秒を考慮しなくともよければ Unix time は内部データ表現の有力な選択肢になるでしょう。解釈にあいまいさがなく、使うメモリも少なく、秒や秒未満の計算は簡単におこなえます。たとえばイベント発生時刻の記録などには Unix time で十分です。

注意点としては longdouble などの単純な数値型で Unix time を保持していると、時差の計算が混ざったときに混乱しがちです。また、よくある誤解に Unix time を「現地時刻の 1970年 1月 1日 0時 0分 0秒」からの経過時間のことだと思ってしまう、というものがあります。 Unix time 専用のクラスや型がある場合はそちらを使うと、混乱や誤解の入り込む余地をなくせるでしょう。 [7]

Unix time そのものでなくても、「Unix time に変換可能」な形式であれば Unix time に近い恩恵を受けられます。たとえば「年・月・日・時・分・秒」と地域ペースではない「オフセット」を組み合わせた形式 [8] では、「存在しない時刻」や「二重に存在する時刻」の心配をする必要がなく、タイムゾーンの呪いを遠ざけることができます。

ただし Unix time も「Unix time に変換可能」な形式も銀の弾丸ではありません。次に検討するように、うるう秒を無視できても、これらが適さないケースがあります。

暦の計算、タイムゾーンの切り替わりをまたぐ計算をするか?

「翌日」や「同月の最終営業日」のような、暦の計算が必要なことがあります。そんなときは Unix time や「Unix time に変換可能」な形式では不足で、地域ペースのタイムゾーンが必要かもしれません。夏時間など、タイムゾーンの切り替わりをまたぐ可能性があるからです。そういうときは、タイムゾーンの呪いがセットでついてきます。サモアのような例を思い出すと、「次の日」自体が存在しないことすらあります。

まずは要件を詳細に洗い出しましょう。たとえば要件が「翌日」となっていても、それが「24時間後 (うるう秒を無視できれば 86400秒後)」でいいのか、それとも「次の日の同じ時刻」なのかで、やることは大きく変わります。そしてうるう秒のときと同様、できるだけ後者を避けて前者に寄せられないか考えましょう。前者であれば Unix time で秒の計算をするだけでもいいし、「次の日の同じ時刻」がそもそも存在しなかったり二重に存在したりするタイムゾーンの呪いも、考えなくてよくなります。

暦の計算をした結果が「存在しない時刻」や「二重に存在する時刻」そのものにはならなくても、たとえば夏時間の切り替わりをまたぐと、「次の日の同じ時刻」までの時間は 23時間かも 25時間かもしれません。日本で 2018年に検討された二時間差の夏時間の例では 22時間かも 26時間かもしれません。「次の日の同じ時刻」までの時間が短くなると、いわゆる「バッチの突き抜け」が起こりやすくなる、という問題もありますね。

それでも「次の日の同じ時刻」のような暦の計算が必要な場合、まずタイムゾーン計算を自分で実装するのはやめましょう。「年・月・日・時・分・秒」と地域ベースのタイムゾーンを組み合わせて時刻を保持した上で [9] 計算はライブラリにまかせるのが第一です。

しかし、ライブラリにまかせさえすれば万事解決というわけではありません。「次の日の同じ時刻」が存在しなかったり二重に存在したりという例外ケースに対してどういう挙動をさせるのかは、そもそも要件と自分たちの決断次第です。たとえばエラーにする、その日を無視してさらに次の日にする、できるだけ切り替わり前のオフセットに寄せる、できるだけ切り替わり後のオフセットに寄せる、できるだけ「標準時」に寄せる、できるだけ「夏時間」に寄せる [10] などなど、さまざまな選択肢があります。いまの要件に対してもっとも適切な挙動はなんでしょうか。要件とにらめっこして考えましょう。

「地域ベースのタイムゾーンを組み合わせて時刻を保持」と書いたように、暦の計算をするときには地域ベースのタイムゾーンが必要になります。地域ペースのタイムゾーンに加えて -07:00 のようなオフセットをともに保持しておくことで、タイムゾーンの呪いを少し軽減できます。 [11] オフセットがないと、呪いのせいで時刻を確定できないことがあるからです。

たとえばカリフォルニア州が夏時間から標準時に戻った 2020-11-01 の時刻を 2020-11-01 01:30:00 America/Los_Angeles とオフセット無しで保持していると、これが夏時間 (-07:00) だったのか標準時 (-08:00) だったのか、特定できません。

オフセットを確定すること、「Unix time に変換可能」と意識しておくことは、多くのケースで有効そうですね。しかしこれすらも銀の弾丸ではありません。オフセットを確定するべき「ではない」ケースがあります。

確定した過去の時刻か? 未来に予定された時刻か?

「時刻」は、大きく二種類に分けることができます。過去に起きた事象の記録としての確定した時刻と、未来に起こる予定の事象の時刻です。この二つは、意味合いが大きく異なります。「毎週火曜日の正午」のような繰り返しの場合、未来の予定はあくまで予定です。

過去に起きた事象とその時刻は、事実として確定しています。 [12] また、ある時点で最新の tzdb は、少なくともその時点より過去について、正しいタイムゾーン情報を持っています。 [13] そしてその tzdb を用いて確定した時刻は世界共通の時間軸上の一点に対応するので、つまり過去の時刻は Unix time や「Unix time に変換可能」なオフセットつきの時刻で表現できるでしょう。

とはいえ過去の時刻についても、補足情報としてその事象が起きた地域 (タイムゾーン) を保持しておくと、いいことがあるかもしれません。たとえば過去の記録を人が確認するとき、その人がいまいるタイムゾーンの時刻でではなく、その事象が起きた場所の現地時刻で確認したいことは多いでしょう。さらに、地域ベースのタイムゾーンがあれば、そこから暦の計算をすることもできます。

地域 (タイムゾーン) だけではなくオフセットも保持する選択肢もあります。しかし、過去の Unix time と地域ベースのタイムゾーンを組み合わせればそのときのオフセットは確定できるので、オフセットは必須ではないでしょう。ただし「過去の Unix time」ではなく「過去の年・月・日・時・分・秒」の形式にしてしまうと、地域ベースのタイムゾーンと組み合わせてもオフセットを確定できなくなることに注意してください。

一方、未来に起こる事象の予定の時刻には、たとえば「次に観測できる皆既日食・金環日食の開始時刻」や、「次のオリンピック開会式の開始予定時刻」のようなものがあります。

未来に起こる予定の時刻をあつかうときは、その時刻の意味合いを慎重に考える必要があります。その時刻は、絶対的に決まっている時刻でしょうか。それとも、どこかの現地時刻として定義・予定された時刻でしょうか。

この問題を短く説明しようとすると、「現地時刻で定義された未来の時刻を、その予定時刻より古い tzdb を用いてオフセットを確定して (または UTC に変換して) 保持し、それをあとから新しい tzdb を用いて現地時刻に戻そうとすると、本来の予定時刻とずれることがある」ということになります。それは、ある時点で最新の tzdb をもってしても、その時点より未来のタイムゾーン情報が「正しい」ことは誰にも保証できない、という単純な事実によるものです。

たとえば、次に日本から観測できる皆既日食・金環日食は、日本時間の 2030年 6月 1日で、札幌から中心食を観測できるのがこの日の日本時間 16時 54分から 16時 58分までだそうです。月も地球も日本の時刻制度のことを気にしながら回っているわけではないので、仮にこのときまでに日本に夏時間が導入されても、その絶対時刻は変わりません。もし夏時間が導入された場合は、紹介文の日本時間での表現を直す、ということになります。このような時刻は、オフセットを確定して「Unix time 変換可能」な形式で保持しても安全です。

しかし別の例として、延期前の東京 2020 オリンピック開会式の開始予定時刻は 2020年 7月 24日 20時だったそうです。 [14] この開始時刻は、現地時刻 (日本時間) で決まっていたと思われます。つまり、仮にこの開始時刻が発表されたあと開会式前に夏時間制が導入されていたら、開始時刻は「日本時間 20時」のままで、絶対時刻のほうが変わるのです。 [15]

このように、未来の時刻のオフセットを (その予定時刻よりも前の) 「現在」わかっている tzdb をもとに「確定」してしまうと、その予定時刻の定義次第で、破滅が起こるかもしれません。 [16]

これはたとえば、各国や地域の「証券取引所の取引時間」などでも同様です。証券取引所の取引時間は、基本的には現地時間です。 2021年時点でニューヨーク証券取引所の取引時間はニューヨーク時間 (東部時間) の 9時 30分から 16時と定義されていて、ニューヨークが標準時の期間でも夏時間の期間でも、これはニューヨーク時間の 9時 30分から 16時です。仮に将来ニューヨーク州が夏時間制を廃止したら、取引時間はおそらく「夏時間制廃止後の現地時間」の 9時 30分から 16時になるのでしょう。

このような未来の時刻は、オフセットを確定せずに、「年・月・日・時・分・秒」ベースの時刻表現と地域ベースのタイムゾーンで保持しなければなりません。

地域ベースのタイムゾーンで保持するしかありませんが、だからといってタイムゾーンの呪いは許してくれません。極端な場合、タイムゾーンがサモアの例のように想定外に変わって、もともと現地時刻で決まっていた予定時刻が存在しなくなってしまうかもしれません。

といってもそんなことがあったら、ソフトウェアとは関係なく、その時刻にかかわっていた人はみんな困っていることでしょう。そんな影響の大きいタイムゾーンの変更が決まったら、それぞれが予定時刻の変更などの特別な対応をおこなうでしょう。ソフトウェアが気をつけることは、仮にそんな変更が起きても壊れたデータを作って永続化してしまうことがないようにし、そして問題を検出できるようににしておく、といったところでしょうか。

データの出力と永続化

時刻データの永続化や出力、他のコンポーネントへの受け渡しでも、考えることの基本は内部データ構造の場合と同様です。

ただし、特に永続化したデータや受け渡したデータには、「自分以外の読み手」がいることが常に想定されます。そのデータが意図しない使いかたをされるのを防ぐためには、以下のことに気をつけるといいでしょう。

  • 「Unix time に変換可能」な形式にできる場合は、できるだけそうする。
    • Unix time 専用のクラスや型があるなら、それを使う。
  • タイムゾーン情報を省略しない。常に UTC と決めていても UTC を明記する。
    • Unix time 専用のクラスや型を使う場合、既に明示されていると言える。
  • オフセットを確定するべきではないケースを除いて、オフセットを明記する。
    • 未来の時刻など、オフセットを確定するべきではないケースもあることに気をつける。

時刻の文字列表現

出力先が RDB などの型がある世界であれば、時刻用の型があることが多いでしょう。しかし、時刻を JSON などに入れなければならないこともあります。そのときは文字列や数値などにエンコードしなければなりません。

文字列にするときは独自のフォーマットを作らず、規格化されたものを使うに越したことはありません。一昔前は、メールなどで使われる RFC 2822 形式 (Fri, 27 Dec 2002 09:25:00 +0900 など) がよく使われていましたが、現在では ISO 8601 に沿った形式 (20211231T123456+09002021-12-31T12:34:56+09:00) のほうが主流です。 Java の JSR 310 も ISO 8601 を強く意識して作られています。

ただし ISO 8601 には、地域ベースのタイムゾーンに関する仕様はありません。 JSR 310 では 2021-12-31T12:34:56+09:00[Asia/Tokyo] のように表記を拡張していますが、そこは標準があるわけではないようです。

「知識編」に引き続き、くどいですが、タイムゾーン略称 (JSTPST など) を使うのはやめましょう。

データの入力

現在時刻

「時刻を取得する」というとき、典型的なのは現在時刻の取得ですね。そのソフトウェアが動いている環境の現在時刻を取得するだけなら、ほとんどの場合は言語の標準ライブラリなどで取得できるでしょう。

しかし、現代のソフトウェアの多くはそんなに単純ではありません。サーバーなら、その動作環境に加えてクライアントの時刻とタイムゾーンを、クライアントなら逆にサーバーの時刻とタイムゾーンを、それぞれ気にしなければならないことが多いでしょう。

それらをごちゃごちゃにしないように、時刻データには早急にタイムゾーン情報を組み合わせる大原則を思い出して、タイムゾーンなしの「年・月・日・時・分・秒」を持ち回らないようにしましょう。

サーバー・クライアントの場合、「どの時刻を現在時刻として信頼するのか」を取り決めておくのも重要です。パソコンやスマートフォンの時刻を自動で合わせるようにしていない人は意外といますし、かといって「クライアントがどういう認識の時刻で送ったデータか」というのも重要な情報ではあります。まあこれは、時刻とタイムゾーンというより、どちらかというと分散システムの話でしょうか。

外部データ

自分で出力・永続化するデータは気をつけて設計しましょう、というのが「データの出力と永続化」で検討した内容です。しかし、外部から受け取れるのが、自分が設計したように理想的なデータばかりとはかぎりません。他の人がカラム設計した RDB から、年・月・日・時・分・秒がバラバラのカラムに入った (しかも整合性が取れていないことがある) テーブルを読まなければならないかもしれませんし、タイムゾーンが不明な TIMESTAMP WITHOUT TIMEZONE 型のカラムを読まなければならないかもしれません。タイムゾーン情報のない時刻 (なぜかカリフォルニア時間) を返してくる Web サービスの API を呼び出さければならないかもしれません。いずれの場合も、大原則にしたがってできるだけ早期に、その時刻がどのタイムゾーンのものか確定してから持ち回りましょう。

受け取る時刻が文字列の場合、前述の ISO 8601 にしたがっていれば簡単ですが、そうでないこともあります。ありがちな問題としては、アメリカ合衆国の慣習にならって、年・月・日を「月・日・年」で並べた文字列がやってくるかもしれません。いずれにせよ、そのデータソースのドキュメントや仕様を確認するのを怠らないようにしましょう。そして、受け取ったらすぐにタイムゾーン情報と組み合わせましょう。

悪夢のタイムゾーン略称が入った文字列を受け取ることも、残念ながらあります。その場合も、やはりそのデータソースのドキュメントや規約を確認しましょう。運がよければ、「CSTUTC-06:00 のこと」や「CSTUTC+08:00 のこと」など、規定してくれているかもしれません。そこが明示されていない場合、略称がそれぞれどのタイムゾーンに対応するのか、対応関係を自分たちでメンテナンスし続けなければならないかもしれません。覚悟を決めましょう。 [17]

ユーザー入力

ユーザーが時刻を入力するとき、その時刻は、どの地域 (タイムゾーン) のどのオフセットのものでしょうか。

まず、そのときのユーザーの意図をどう想定するか、要件から検討しなければなりません。特に、前述のように未来の予定時刻を入力する場合、ユーザーは「絶対的に決まっている時刻」を入力したいと想定しているのか、「現地時刻で定義・予定された時刻」を入力したいと想定しているのか、その目的と要件によって異なるでしょう。

ユーザーが入力したいタイムゾーンを推定・確定する方法はいくつか考えられます。大きく分けると、時刻と一緒に手動で入力してもらう、ユーザー・アカウントなどに事前に設定しておいてもらう、ユーザーが「いまいる」タイムゾーンを使う、の三通りくらいではないでしょうか。

手動で入力してもらうのは、きわめて煩雑ですが確実です。限定された「わかっている」ユーザーが対象の場合や、間違いが許されない重要な設定の場合は、これが有力な選択肢になります。また、おもな手段としては手動以外を採用するものの、オプションとして手動で指定する余地を残す、という設計もあるでしょう。手動で入力はさせないまでも、「いまあなたが入力しようとしているのは東京時間です」という表示だけでも、間違いを減らせるかもしれません。これは見た目とのトレードオフになるでしょう。

たとえば Amazon RDS のメンテナンスウィンドウ設定は、時刻を手で入力するようになっています。 2021年現在は UTC 固定になっていますが、昔はタイムゾーンを手動で指定するようになっていました。これは、「限定された『わかっている』ユーザーが対象」で、かつ「間違いが許されない重要な設定」なので、妥当な設計に見えますね。タイムゾーンを指定可能と言っても、地域ベースのタイムゾーン (America/Los_Angeles など) は選択肢になく、オフセット (-07:00 など) のみだったのですが、その理由は、ここまで読んでいただいた方ならわかると思います。

ユーザー・アカウントなどに設定しておいてもらう方法は、もちろんアカウントなし一回利用 (one-time) のシステムでは使えません。そのうえ、ユーザーが旅行などで他のタイムゾーンに移動したときに、ユーザーが設定の追従を忘れる可能性も考慮しなくてはならないでしょう。

また、このようなユーザー・アカウントへの設定は、地域ベースのタイムゾーンにするのが自然でしょう。カリフォルニアに住んでいる人が、夏時間や標準時に切り替わるたびに手動で設定を変更するとしたら、ちょっと滑稽です。しかし地域ベースのタイムゾーンを使えば、いつものタイムゾーンの呪いがやってきます。 America/Los_Angeles と設定したユーザーが「2020年 3月 8日 午前 1時 30分」と入力したら、どうすればいいでしょうか。これも要件次第で対応を考える必要があります。

Google Calendar は、アカウントに設定済みのタイムゾーン (地域) の外からアクセスすると、ユーザーが「いまいる」タイムゾーンを検出して「このアカウントのタイムゾーンを○○に更新しますか?」と聞いてくれます。黙って設定済みのタイムゾーンのままで処理されるのも困りますし、勝手に変更されるのも困ることがあります。確認を表示するのは有効なやりかたですね。

ユーザーが「いまいる」タイムゾーンを使うのは、意外と厄介だと思っておきましょう。

まず、「ユーザーが実際にいまいる場所」と「ユーザーが入力したいタイムゾーン」が一致しているかは、自明ではありません。旅行から帰ったあとの予定を旅行先から入れたい場合、異なるタイムゾーンにいる上司との定期ミーティングを設定したい場合、違う地域にあるサーバーのメンテナンス時間を設定したい場合など、いろいろなケースがあります。

そして「ユーザーがいまいるタイムゾーン」として得られる情報も、確実なものではありません。スマートフォンは通信回線と現在位置からタイムゾーンを推定するので、スマートフォンのアプリなどから取得できる情報はそこそこ信頼できます。しかし、たとえば頻繁に旅行や出張をする人の中には、ノートパソコンのタイムゾーンを常に居住地に合わせていて、現在地を反映しているわけではない人もけっこういます。

さらに、少し前までブラウザから安定して取得できるタイムゾーン情報はオフセットのみでした。ブラウザから tzdb 形式のタイムゾーン ID を取得する API (Intl.DateTimeFormat().resolvedOptions().timeZone) がほとんどのブラウザでサポートされるようになったのは最近のことです。しかも API ができたとはいえ、それは結局「そのユーザーの設定」以上の信頼性はありません。

「ユーザーがいまいるタイムゾーン」として得られた情報を、確実な情報だと考えてはいけません。「ヒント」程度に受け取っておきましょう。時刻をブラウザに表示するときや、入力の「デフォルト」に用いるくらいならよさそうです。しかし、特に永続化するデータの根拠として用いるのは危険だと考えたほうがいいでしょう。

tzdb の更新

tzdb の新しいバージョンが頻繁にリリースされているのは、「知識編」でも触れたとおりです。古い tzdb データを使っていると、一部の時刻の解釈が各国や地域の実情に合わなくなって、致命的な問題につながるかもしれません。

ネイティブのアプリケーションやライブラリなどであれば、その開発と tzdb のバージョン管理とは独立です。 tzdb データの更新は、そのアプリケーションやライブラリを動かすホスト・端末・環境の管理者の責任です。しかし Web サービスを開発・運用しているなら、そんなことは言っていられません。サービス運用の一環として tzdb 更新の戦略も立てておかなければなりません。

tzdb のデータは OS や Java などの実行環境、ライブラリなどに、それぞれ埋め込まれています。開発しているサービスが、どこにある tzdb データを参照していて、なにを更新すればサービスが使う tzdb データが更新されるのか、把握しておきましょう。

たとえば Java が使う tzdb データは Java の実行環境と一体になっています。 JDK などの Java 実行環境を更新すれば、その tzdb データも更新されます。更新が必要なのは実行環境であって、アプリケーションのビルド環境ではないことに注意しましょう。

Ruby で tzdb ベースのタイムゾーン情報をあつかうには tzinfo gem を使います。関連 gem に tzdb データを埋め込んだ tzinfo-data gem があり、これが見つかるか見つからないかで挙動が変わります。 tzinfo は、この tzinfo-data が見つかるとそのデータを優先的に使い、見つからないと OS の tzdb データ (zoneinfo) を探して使う、という動作になります。 Ruby on Rails のアプリケーションを運用している場合、この tzinfo-data を使っているのか OS のデータを使っているのか、なにか事故が起きる前にちゃんと把握しておきましょう。

まとめ

先の「知識編」からこの「実装編」にかけて、時刻とタイムゾーンの一般的な知識から、ソフトウェア実装時の一般論まで検討してきました。

いかがでしたか?

合わせてかなり長大な記事になってしまいましたが、この長さだけでも、時刻とタイムゾーンという概念を正しくあつかうのは意外とたいへんなんだ、ということが伝わっていれば、書いたかいがあります。この内容をすべて常に頭に入れておく必要はないと思いますが、時刻にかかわるプログラムを実装しなければならなくなったとき、この記事のことを思い出していただけたらと思います。

一般論はここで終わりですが、もともと Qiita に載せていた 2018年版の旧記事では、さらに Java を具体例とした実装の話もしていました。その内容も、改訂の上で第三部「Java 編」として別記事にしてあります。

普段 Java を使う方や、興味のある方は、こちらもご覧ください。

脚注
  1. Stack Overflow の質問 "Daylight saving time and time zone best practices" が膨大な長さになっているのを見ても、シンプルな「ベスト・プラクティス」なんて無理だ、というのが伝わるかと思います。 ↩︎

  2. ときどき JSR 310 を参考にはします。 ↩︎

  3. Java JSR 310 で言えば java.time.LocalDateTime など。 ↩︎

  4. 銀行や金融など? ↩︎

  5. そもそも、うるう秒を直接あつかえるライブラリが多くありません。たとえば JSR 310 も、単体でうるう秒をあつかうには不十分です。どうしてもうるう秒をあつかわなければならない場合、それでもできるだけいいライブラリを見つけてきてライブラリにまかせるほうが、自分で実装するよりは何倍もマシです。 Java の場合は JSR 310 と「Java 編」で紹介する ThreeTen-Extra という外部ライブラリの組み合わせで、うるう秒もある程度カバーできます。 ThreeTen-Extra はもともと JSR 310 の一部として検討されていたクラス群ですが、その JSR 310 があまりに巨大化したために整理され、外部ライブラリとして切り出されたものです。 ↩︎

  6. Java なら System.nanoTime()など。 ↩︎

  7. Java JSR 310 なら java.time.Instant など。 ↩︎

  8. Java JSR 310 の java.time.OffsetDateTime など。 ↩︎

  9. Java JSR 310 の java.time.ZonedDateTime など。 ↩︎

  10. とはいえ、できるだけ「標準時」や「夏時間」に寄せることにしても、サモアのように標準時自体が変わることもあるし、夏時間が廃止になることもありますね。 ↩︎

  11. Java JSR 310 の java.time.ZonedDateTime のインスタンスには、補助情報としてオフセット (ZoneOffset) も保持できます。 ↩︎

  12. 相対論とかシュタインズ・ゲートの選択とかは忘れましょう。 ↩︎

  13. tzdb の過去の情報に間違いが見つかることもあるので、これは実は確実に正しいわけではありません。しかし常にそこまで疑ってかかるのはあまり現実的ではなく、ひとまずその可能性を無視した設計にするのは許容範囲なのではないかと思います。間違いが見つかるのは、多くは直近の情報ではなく、たとえば 1980年代など一昔前の情報であることが多い、というのもあります。 ↩︎

  14. 「開会式・閉会式は午後8時から 東京五輪、日程決定」 (2019年 4月 16日、日本経済新聞) ↩︎

  15. もっとも、実際にこの開始時刻が決まったのは夏時間の導入を断念したあとでしたし、仮にそうなっていても、スポンサーや各国放送局の都合などで日本時間のほうをずらしたのでしょうが。 ↩︎

  16. Jon Skeet による "STORING UTC IS NOT A SILVER BULLET" (Mar 27, 2019) では、「知識編」でも触れた EU の夏時間廃止を題材として、この問題を議論しています ↩︎

  17. ちなみに Java JSR 310 の java.time.format.DateTimeFormatterBuilder#appendZoneText は、タイムゾーン略称を受け取ると Locale などから判断してそれらしきタイムゾーンを選んでくれるのだそうです。ありがた (迷惑) ですね。実行環境によって結果が変わるなんて、悪夢でしかありません。これ、テスト環境では動いてたんだよ! ↩︎

Discussion

Tsuyoshi CHOTsuyoshi CHO

大作お疲れさまです。

「時刻はとりあえず UTC に変換しておけ」とはよくいわれますが、それは間違ったベスト・プラクティスです。

ベストではないので間違っているというのはありますが、一部ではあるので、「不十分な」とかくらいのほうがいいかもとは思いました。

JSR 310 では 2021-12-31T12:34:56+09:00[Asia/Tokyo] のように表記を拡張していますが、そこは標準があるわけではないようです。

この記事 にもありますが 提案仕様がIETFに提出されているみたいです。どうなるかはわかりませんが。

Dai MIKURUBEDai MIKURUBE

コメントありがとうございます。

ベスト・プラクティスについては、「『とりあえず』でそのプラクティスに従ってしまうとかえって問題を起こすことがある」という意味で、あえて「間違った」と強い表現にしています。「問題までは起こさないけど常にいいわけでもないよ」くらいなら「不十分な」くらいにしたかもしれませんが。

その提案仕様については少し眺めましたが、なにかしら標準になるといいなー、と思っています。標準化には一般に時間がかかると思いますが。 JavaScript の temporal もなかなかいいものになりそうな雰囲気なので、期待したいですね。