Help us understand the problem. What is going on with this article?

時刻には「必ず」タイムゾーンをつけて扱え!絶対だぞ!?

プログラムで時刻を扱うときは、必ずタイムゾーン付きの方の時刻型を使いますよね?

「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が、JSTなのかGMTなのか分からないからです(常識的にはGMTですけど、宵っ張りの友人があえて深夜を指定する可能性もあります)。

私たちは時刻を時間という直線の上の1点のように捉えますが、12:00とか2019/12/04 14:09:24といったタイムゾーンを含まない文字列からは、それが直線上のどの1点のことかは定まりません。タイムゾーンの情報が加わって、初めて時刻が得られます。

では12:002019/12/04 14:09:24は何を表す文字列なのでしょうか?時刻ではありません。専門的には名前がついているかもしれませんが、とりあえず、直感的ではない何かです(「時刻 - タイムゾーン」?)。

同様に、タイムゾーンを持たない datetime without timezone 型も、よく考えれば何を表すのか分からない型なのです。

datetime with timezone型は意味が明確

一方、2019-12-04T14:09:24+0900のようなタイムゾーン付き表記は時刻を表していることが明確です1

「イギリス時間昼11: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を提供しないものがあり、使いたくても使えませんでした。

メモリやストレージが高価な時代には、少しでもオブジェクトサイズを小さくする必要がありました。タイムゾーンを省けば、1オブジェクトあたり数バイト節約できます。

しかし、現代では、

  • 世界中にサービスを提供するのが当たり前
  • 国内向けサービスでも、クラウド経由で海外サーバーを使うのが当たり前
  • 大抵の言語はdatetime with timezoneを提供しています
  • メモリやストレージを気にする必要もありません(少なくとも "+0900" をチマチマ削る時代ではありません)

普通にdatetime with timezoneを使えばいいと思います。

「タイムゾーンなしの時刻」が現れるシーンは限られる

タイムゾーンを明示しない時刻が本当に必要となるシーンは限られます。現実的には3種類しかないと思います。

1つは、レガシーを扱うときです。

2つ目は、ユーザー向けに時刻を表示するときです。スマホのステータスバーに、時刻が「04:00+05:00」と表示されたらイラっとしますよね(いや、日本時間でいいからさ!!)。ユーザーに時刻を表示するときは、ユーザーのタイムゾーンに変換して表示し、タイムゾーンは省略します。

3つ目は、ユーザーに時間を入力させるときです。「ヘイSiri、明日の9時にデートの予定」に、「9時とはどこの時間ですか?それにワシントンはまだ昨日ですよ?」と返すSiriはいません。ユーザーの入力内容は、ユーザーのタイムゾーンのものとして解釈します。

他にも、トリビアルな例(「目覚ましアプリでは『朝7時』は常に現地時間を意味する」など)はあるでしょうが、基本的には上の3種類以外にdatetime without timezoneが出現する機会はないのではないでしょうか?

むすび

悪いことは言いません。時刻にはタイムゾーンをつけて扱いましょう。

Q&A形式の補足

Q. 「地球上の全ての場所で同じ時刻」という前提は厳密には誤りでは?

A. ご存知の通り、NTPではミリ秒程度の誤差が発生しますし、相対論的効果で時間の流れ方は変化します。「時間は存在しない」なんて本もあります。

そのため、

  • GPSのようなマイクロ秒単位の精度が必要な場合
  • Google Spannerのような分散システムを構築する場合
  • サーバーが亜光速で移動する場合
  • 素粒子レベルの世界を扱う場合

などには「同じ時刻」云々は厳密には誤りです。

とはいえ、「イギリスの友人に電話」のような日常的状況では、「全ての場所で同じ時刻」と仮定して良いでしょう。

Q. うちの古いDBでは datetime without timezone型になってるんだけど・・・

ご愁傷様です。

datetime without timezone だったとしても、暗黙にUTCやAsia/Tokyoといったタイムゾーンを仮定しているはずので、DBアクセスの前後にwith timezonewithout timezoneを変換するラッパー層を設けて、ビジネスロジックを守ることができるはずです。

また、RailsなどのFWにはこういったケースに対応する機能があります。

Q. 古いツールがタイムゾーンを無視するんだけど

例えば、タイムゾーンを変換せずに、

select to_char(created_at, 'YYYY-mm-dd HH:MM') from ...;

のような、クエリを発行していたりすると、created_at列の型をdatetime with timezoneに変換したときにバグるかもしれません。

本来、DBから時刻を取り出すときは、オブジェクトとして取り出すべきで、整形済み文字列を取り出すべきではないのです。あるいは、タイムゾーンを変換した上で整形するべきなのです。

これについては「そうだよね」としか言えません。ツールを直すか、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、タイムゾーンという要素を含みません。


  1. 正確には「日本時間である」という情報が含まれているので、時刻以上の情報を含んでいます。 

  2. 閏秒などの扱いがあるので、厳密な定義ではありません。 

tonluqclml
エムスリーでソフトウェアエンジニアしています。仕事ではRubyもScalaもPythonもBashもなんでもやる雑食系。 Twitter:https://twitter.com/doloopwhile 昔の個人ブログ:http://doloopwhile.hatenablog.com/ 勤務先ブログ: https://www.m3tech.blog/
http://doloopwhile.hatenablog.com/
m3dev
インターネット、最新IT技術を活用し日本・世界の医療を改善することを目指します
https://m3.recruitment.jp/engineer/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away