タイムゾーンとDST、IANA tzdata の罠:時刻処理で詰まらないために

約7分

タイムゾーンは「単に UTC からのオフセットを足すだけ」と思いがちですが、実際には DST(夏時間)切り替え、過去のオフセット変更、政治的決定による変動などが絡み、想像以上に複雑です。本記事では時刻処理で頻発する罠と、それを避けるための設計指針を整理します。

タイムゾーンの正体:単なるオフセットではない

「タイムゾーン」と聞くと多くの人が UTC+9 のような固定オフセットを想像しますが、実際のタイムゾーンは:

  • 地理的な領域(例:Asia/TokyoAmerica/New_York
  • 過去から現在までのオフセット変更履歴
  • DST の有無と、その切り替えルール

を含む概念です。同じ「ニューヨーク」でも夏は UTC-4、冬は UTC-5 で、年によって切り替え日も変わります。

IANA tzdata:世界のタイムゾーン情報の正準データベース

世界中のタイムゾーンとその履歴は IANA Time Zone Database(通称 tzdata、または zoneinfo)に集約されています:

  • 履歴データ:過去の DST 切り替え、オフセット変更、夏時間の廃止・再開
  • 約 600 のゾーン定義
  • 年に数回更新(政治的決定があるたび)

ゾーン名の例:

  • Asia/Tokyo — JST、DST なし、UTC+9 固定
  • America/New_York — EST/EDT、DST あり
  • Europe/London — GMT/BST、DST あり
  • UTC — オフセット 0、DST なし
  • Etc/GMT+5 — UTC-5(符号が反転している点に注意:POSIX 互換のため)

OS や Java、Python、JavaScript の Intl.DateTimeFormat 等は内部で tzdata を使っています。

なぜ DST が時刻処理を難しくするのか

DST の切り替え時刻には、現実には存在しない時刻2 回出現する時刻が発生します。

春の切り替え:時刻が「飛ぶ」

ニューヨークで 3 月の第 2 日曜:

02:00 EST → 03:00 EDT

つまり 02:30 EST は存在しません。「2:30 AM」をパースしようとすると:

  • ライブラリによっては「3:30 AM EDT」に補正
  • 別のライブラリでは「2:30 AM EST」として扱う(前のオフセット)
  • エラーを投げるライブラリもある

秋の切り替え:時刻が「重複」

ニューヨークで 11 月の第 1 日曜:

02:00 EDT → 01:00 EST

01:30 が 2 回出現します。「1:30 AM」の指定だけでは、UTC で 05:30 か 06:30 か判別できません。

実装での影響

// JavaScript で「2024-03-10 02:30」をパース(米国東部)
new Date('2024-03-10T02:30:00-05:00'); // EST → 03:30 EDT に補正
new Date('2024-03-10T02:30:00-04:00'); // EDT 扱い

ローカル時刻だけ与えられた場合、どのオフセットを使うかを推測する必要があるのが本質的な問題です。

過去のオフセット変更:tzdata 履歴の必要性

「東京の 1948 年の時刻は?」と聞かれると、実は単純に UTC+9 ではありません。日本は 1948〜1951 年に DST を実施しており、Asia/Tokyo のゾーンは:

1948-05-01 ~ 1948-09-11  JDT (UTC+10)
1948-09-12 ~ 1949-04-02  JST (UTC+9)
1949-04-03 ~ 1949-09-10  JDT (UTC+10)
...
1952-01-01 以降           JST (UTC+9) 固定

このような履歴を正確に扱うには tzdata 必須で、固定オフセットでは表現できません。

政治的変動:タイムゾーンは予告なく変わる

タイムゾーンは政治的決定で予告なく変わります:

  • 2011 年:サモアが UTC-11 から UTC+13 に変更(日付変更線をまたぐ)
  • 2014 年:ロシアの一部地域が DST を廃止し、永久標準時に
  • 2024 年:複数の国が DST 廃止を検討中

これらの変動が起きると tzdata がリリースされ、各 OS / ランタイムにデプロイされます。古いシステムは古い tzdata で動くため、未来の日時を計算したときに新ルールを反映しないことがあります。

API 設計:時刻をどう保存・転送するか

これらの罠を踏まえると、API では以下の原則が安全です。

1. 時刻はすべて UTC で保存

データベースに保存する時刻は UTC(または timestamptz)で統一。タイムゾーンは「表示時刻に変換する直前」に適用。

2. 表示時刻はゾーン名 + UTC 値で表現

「2024-04-01 12:00 JST」を伝えるには:

{
	"timestamp": "2024-04-01T03:00:00Z",
	"timezone": "Asia/Tokyo"
}

UTC 値 + ゾーン名の組み合わせ。固定オフセット +09:00 でも JST については正しいですが、DST のあるゾーンでは曖昧です。

3. ISO 8601 を使う

2024-04-01T12:00:00+09:00 のように ISO 8601 + オフセット形式は:

  • パーサーが多言語で揃っている
  • 人間が読める
  • 一意に時刻が定まる

「2024/04/01 12:00:00(JST)」のようなロケール依存表記は API では避ける。

4. ユーザーのタイムゾーンはブラウザから取得

クライアント側で Intl.DateTimeFormat().resolvedOptions().timeZone で IANA ゾーン名が取れます(例:Asia/Tokyo)。これをサーバーに送ってサーバー側で表示時刻に変換するのが安定的です。

JavaScript での時刻処理の現状

JavaScript の Date オブジェクトは長く時刻処理の弱点でした:

  • ローカルタイムゾーンと UTC しか扱えない
  • 任意のゾーンへの変換が困難
  • イミュータブルでない

近年の状況:

  • Moment.js:かつての標準だが既にメンテナンスモード
  • date-fns / dayjs:軽量な代替、ゾーン操作は別パッケージ
  • Luxon:豊富なゾーン操作 API
  • Temporal(提案中の標準 API):将来的にネイティブで使えるようになる予定

新規プロジェクトで複雑なゾーン処理が必要なら Luxon、Temporal が安定するまでは date-fns + date-fns-tz が現実的な選択肢です。

まとめ:時刻処理のチェックリスト

  • DB に保存する時刻は UTC で統一する
  • 表示時刻は IANA ゾーン名と一緒に管理する
  • DST 切り替え時刻のテストケースを書く(特に春の「飛ぶ」時刻)
  • tzdata の更新を反映する仕組みがあるか確認
  • ISO 8601 形式を API のデフォルトにする
  • ユーザーのゾーンは Intl.DateTimeFormat で取得する

UTC、JST、PST、CET など複数のゾーン間の時刻変換を確認したい場面では、本サイトのタイムゾーン変換ツールが手早いです。DST 適用後の時刻も自動で計算されます。