プログラムで時刻を扱うときは、必ずタイムゾーン付きの方の時刻型を使いますよね?
「UTCの時刻に変換してdatetime without timezone
型の列に格納」なんて、レガシー対応以外ではしませんよね!?
REST APIで、パラメータで時刻渡すときは2019-11-29T20:36:37+09:00
みたいにタイムゾーン付きの表記にしますよね!?
「当たり前じゃないか」という方には、この記事は蛇足です。
でも、
- 「
datetime without timezone
で、サーバーのタイムゾーンを使えばいい」 - 「内部ではUTCに揃えて扱う」
- 「パラメータは "2019-11-29T20:36:37"形式で。タイムゾーンはJSTに統一」
とか言いだす人に遭遇することが意外とあります。
次回遭遇したときに備えて(or 次回遭遇を防ぐために)、なぜタイムゾーン付きをつけるべきなのかこの記事で説明したいと思います。
TL;DR
- そもそも「タイムゾーンなしの時刻」って、意味が分からなくない?
- タイムゾーンは表記方法の違いにすぎない。日本語表示・英語表示の違いにすぎない
- 昔はともかく、現在は
datetime with timezone
型を使うのが混乱がなく無難
※おことわり
日時型の名前は言語によって異なるため、この記事では以下のように表記します。
- 日時を扱える型の総称 →
datetime
型 -
datetime
の、タイムゾーン情報を持つ変種 →datetime with timezone
型 -
datetime
の、タイムゾーン情報を持たない変種 →datetime without timezone
型
また「datetime
型のオブジェクト」と、「datetime
型のオブジェクトをシリアライズした文字列(2019-11-29T20:36:37+09:00
)」を一緒くたにしている部分がありますが、この記事で言いたいのは「(オブジェクトであれ文字列であれ)タイムゾーンを付けろ!」なので、多少の混乱はご容赦ください。
そもそも「タイムゾーンなしの時刻」って何?
ここでクイズです。
イギリス在住の友人に「明日12:00に電話をくれ」と言われました。何時に電話をすればいいでしょう?
答えは「問題文の情報からは決定できない」です。12:00
が、GMTの12時なのかJSTの12時(GMTの午前3時)なのか分からないからです(常識的にはGMTですけど、宵っ張りの友人があえて深夜を指定する可能性もあります)。
私たちは時刻を時間という直線の上の1点のように捉えますが、12:00
とか2019/12/04 14:09:24
といったタイムゾーンを含まない文字列からは、それが直線上のどの1点のことかは定まりません。タイムゾーンの情報が加わって、初めて時刻が得られます。
では12:00
や2019/12/04 14:09:24
は何を表す文字列なのでしょうか?時刻ではありません。専門的には名前がついているかもしれませんが、とりあえず、直感的ではない何かです(「時刻 マイナス タイムゾーン」?)。
同様に、タイムゾーンを持たない datetime without timezone
型も、よく考えれば何を表すのか分からない型なのです。
datetime with timezone
型は意味が明確
一方、2019-12-04T14:09:24+0900
のようなタイムゾーン付き表記は時刻を表していることが明確です1。
「イギリス時間12:00に電話をくれ」なら、その意味は明確ですよね?何となれば、我々は地球上の全ての場所で同じ時間が流れていると認識しています。タイムゾーンは、場所(太陽の見え方)の違いであって、時刻の違いではないのです。
だから、特別な理由がない限り、時刻を処理するにはdatetime with timezone
を使いましょう。時刻を文字列で表現するときは、2019-12-04T14:09:24+0900
のような、タイムゾーンつきの表記にしましょう。
datetime with timezone
型で困ることは(基本的には)無い
最近の言語やツールは、ほぼ確実にdatetime with timezone
型を備えています。
Pythonのような歴史が長いツールではdatetime without timezone
が初期設定になっていることもありますが datetime with timezone
も用意されているはずです。
また、datetime with timezone
同士の演算では、タイムゾーンを考慮して演算される(タイムゾーンが自動変換される)ので、コードが複雑になることもありません。「UTCとJSTを混ぜて計算してバグった!」みたいなことも起きません。
# Pythonの例
from datetime import datetime, timezone, timedelta
JST = timezone(timedelta(hours=9))
UTC = timezone.utc
jst_dt = datetime.now(tz=JST) # JST表記での現在時刻
print(jst_dt) # => 2019-12-04 17:17:44.715103+09:00
utc_dt = datetime.now(tz=UTC) # UTC表記での現在時刻
print(utc_dt) # => 2019-12-04 08:17:44.715110+00:00
# 経過秒数を計算すると、ほとんどゼロになるはず。
delta = jst_dt - utc_dt
print(delta.total_seconds()) # => -7e-06
なぜ昔は datetime without timezone
を使っていたのか?
datetime with timezone
の利点を述べましたが、なぜ今まで人類はdatetime without timezone
を使っていたのでしょうか?
まず、IT革命以前にはタイムゾーンをまたぐサービスが少なかったため問題が起きませんでした。
また、メモリやストレージが高価な時代には、少しでもオブジェクトサイズを小さくする必要があり、タイムゾーンを省けば数バイト節約できました。
しかし、現代では状況が変わっています:
- 世界中にサービスを提供するのが当たり前
- 国内向けサービスでも、クラウド経由で海外サーバーを使うのが当たり前
- 大抵の言語は
datetime with timezone
を提供しています - メモリやストレージは潤沢で
"+0900"
をチマチマ削る必要性は薄い
したがって、よほどの理由がない限りは datetime with timezone
を使えばいいと思います。
「タイムゾーンなしの時刻」が現れるシーンは限られる
タイムゾーンを明示しない時刻が本当に必要となるシーンは限られます。現実的には3種類しかないと思います。
1つは、レガシーを扱うときです。
2つ目は、ユーザー向けに時刻を表示するときです。スマホのステータスバーに、時刻が「04:00+05:00」と表示されたらイラっとしますよね。ユーザーに時刻を表示するときは、ユーザーのタイムゾーンに変換して表示し、タイムゾーンは省略します。
3つ目は、ユーザーに時間を入力させるときです。「ヘイSiri、明日の9時にデートの予定」に、「9時とはどこの時間ですか?それにワシントンはまだ昨日ですよ?」と返すSiriはいません。ユーザーの入力内容は、ユーザーのタイムゾーンのものとして解釈します。
他にも、トリビアルな例はあるでしょうが、基本的には上の3種類以外にdatetime without timezone
が出現する機会はないのではないでしょうか?
むすび
悪いことは言いません。時刻にはタイムゾーンをつけて扱いましょう。
Q&A形式の補足
Q. 「地球上の全ての場所で同じ時刻」という前提は厳密には誤りでは?
A. ご存知の通り、NTPではミリ秒程度の誤差が発生しますし、相対論的効果で時間の流れ方は変化します。「時間は存在しない」なんて本もあります。
そのため、
- GPSのようなマイクロ秒単位の精度が必要な場合
- Google Spannerのような分散システムを構築する場合
- サーバーが亜光速で移動する場合
- 素粒子レベルの世界を扱う場合
などには「同じ時刻」云々は厳密には誤りです。
とはいえ、「イギリスの友人に電話」のような日常的状況では、「全ての場所で同じ時刻」と仮定して良いでしょう。
Q. うちの古いDBでは datetime without timezone
型になってるんだけど・・・
ご愁傷様です。
datetime without timezone
だったとしても、「このデータベースでは時刻はJSTで格納する」といったルールがあるはずです。
DBアクセスの前後にwith timezone
とwithout timezone
を変換するラッパー層を設けて、ビジネスロジックを守ることができるはずです。RailsなどのFWにはこういったケースに対応する機能があります。
Q. 古いツールがタイムゾーンを無視するんだけど
例えば、タイムゾーンを変換せずに、
select to_char(created_at, 'YYYY-mm-dd HH:MM') from ...;
のような、クエリを発行していたりすると、created_at
列の型をdatetime with timezone
に変換したときにバグるかもしれません。
本来、DBから時刻を取り出すときは、オブジェクトとして取り出すべきで、整形済み文字列を取り出すべきではないのです。あるいは、タイムゾーンを変換した上で整形するべきなのです。
これについては「そうだよね」としか言えません。
Q. ○○○言語には datetime without timezone
型しかない
(とはいえ、JavaScriptのDate
型はタイムゾーン云々以前に・・・・・・なんでもありません)
対応策としては、以下のようなものが考えられます。
1. サードパーティ・ライブラリを使う
datetime with timezone
型を提供するライブラリがあれば、それを使いましょう。
2. 内部では時刻を常にUTCで扱う
内部処理では、時刻はすべてUTC時刻として扱うことにします(UTCでない別のタイムゾーンでも良いのですが、「ローカルのタイムゾーンとは無関係」がわかりやすいようにUTCにするのが良いでしょう)。
外部から他のタイムゾーンの時刻文字列を受け取った時は、すぐにUTCに変換してから処理します。
3. 内部では時刻をUnix時間(int
型)で管理する
2. の変種ですが、Unix Epochからの秒数であれば、タイムゾーンを取り違えるミスは防ぎやすい(こともあります)。
Q. 「UTCのUnix時間」と「JSTのUnix時間」を取りちがえる危険があるのでは?
余談ですが、上司がUnix時間のことを「現地時間の1970年1月1日午前0時からの経過秒数」と誤解していて、このような質問をされたことがありました。
Unix時間は「UTCでの1970年1月1日午前0時からの経過秒数」なので2、タイムゾーンという要素を含みません。