LabHub

ブログ

時間と日付の技術的地獄 完全解説 — TimeZone、DST、leap second、Temporal API、NTP 徹底ガイド (2025)

한국어English日本語

はじめに — 「2024 年 10 月 27 日 02:30」は存在しないかもしれない

ヨーロッパでサマータイムが終わる日、夜明けの 03:00 になると時計は 02:00 に巻き戻る。つまり同じ日に 02:30 が 2 回 現れる。その日の 02:30 にクーポンを発行したイベントがあったら、そのクーポンはどちらの 02:30 のものだろうか?

逆に、3 月最終日曜のヨーロッパは 02:00 から 03:00 に飛ぶ。02:30 という時刻そのものが 存在しない。ユーザーが「02:30 に予約」と保存したら、その予約はいつ実行されるのか?

これらの問いは単なる UX の問題ではない。金融取引の order ソート、ログの時系列分析、分散システムのイベント ordering、給与計算 のような現場のシステムがこの微妙さで壊れる。Jon Skeet(Noda Time の作者)の有名な言葉: 「Time is a joke. Seriously.」

本稿は、時間と日付を扱う技術的基盤をゼロから解説する。UTC とは何で GMT とどう違うか、Unix time はなぜ leap second を無視するのか、IANA tz データベースがなぜ頻繁に更新されるのか、DST 切り替えをどう安全に処理するか、new Date() の悪名高い落とし穴、JavaScript Temporal API がなぜ 10 年ぶりの革新なのか、分散システムで NTP/PTP により時計を合わせる方法、そして Google Spanner の TrueTime まで。

Unicode の記事で「文字列は難しい問題だ」と書いたなら、時間はそれより難しい。文字列は少なくとも 1 台のコンピュータ内では決定論的だが、時間は地球の自転速度の変化、国家の政治的決定、OS の clock drift まで絡んでいる。


1. 時間の 3 つの概念 — Instant、Local、Civil

プログラムで時間を扱うときにまず区別すべき 3 つ:

1.1 Instant (瞬間)

「今この瞬間」という絶対的な時点。地球のどこで観測しても同じ値になる。Unix timestamp (1970-01-01T00:00:00Z からの経過秒) が代表的表現。

例: 1760000000 (2025-10-09T07:33:20Z に相当する秒単位 epoch)

1.2 Civil Date/Time (民間時刻)

「2026 年 4 月 15 日 13:30」のように カレンダーと時計で読む値。タイムゾーンなしでは曖昧。

1.3 Zoned Date/Time (タイムゾーン付き民間時刻)

「2026-04-15 13:30 Asia/Seoul」。これで Instant に変換できる。

1.4 核心ルール

「UTC で保存すれば OK」は半分正しく半分間違い。「2026 年 12 月 25 日 00:00 にアラーム」というスケジュールをソウルのユーザーが設定した場合、UTC に換算して保存すると DST 変更や tz 変更時に 日付が変わってしまう。こうしたケースでは元の civil+tz を保持する必要がある。


2. UTC、GMT、TAI、UT1 — 時刻標準の混乱

2.1 GMT (Greenwich Mean Time)

英国グリニッジの平均太陽時。19 世紀に船舶航海のため標準化。法的には英国の一部公式文書で今でも使われるが、技術文書では「GMT」は ほぼ常に UTC の口語的同義語 として使われる。

2.2 UTC (Coordinated Universal Time)

国際標準。atomic time ベース で非常に精密だが、地球の自転と合わせるために leap second を挿入する。UTC = TAI - (累積 leap second 数)。

2.3 TAI (International Atomic Time)

世界約 400 台の原子時計の平均。leap second なし。2025 年時点で UTC より 37 秒 先行している。GPS time は UTC ではなく TAI ベースで、1980 年基準のオフセットを持つ。

2.4 UT1

地球自転に実際に合わせた時間。UTC は UT1 と 0.9 秒以上ずれてはならないという規約で leap second が追加される。

2.5 実務まとめ


3. Unix Timestamp — シンプルさの代償

3.1 定義

1970-01-01T00:00:00Z からの経過秒 (または ms/ns)。

3.2 leap second はどこへ消えるのか

実際の地球時間で leap second が挿入されると UTC は 23:59:60 を一度通る。Unix time はこれをどう処理するか?

2 つの方式:

  1. Slew (推奨): leap second 前後の数時間にわたって時刻をゆっくり延ばして吸収。Google/AWS/Meta が採用。どの瞬間も「スキップ」されない。
  2. Smear: NTP サーバが leap second 当日に 86,400 秒ではなく 86,401 秒相当を slew。実際の OS clock もなめらかに進む。
  3. Jump: 23:59:59 から 00:00:00 に戻る。多くのシステムが crash したり重複イベントを生成する。

歴史的事故:

今後: 2022 年に国際度量衡総会 (CGPM) が 2035 年までに leap second を廃止 することを決定。以降は数十年単位で「leap minute」などより大きな単位で調整。

3.3 2038 年問題

Unix time を 32bit signed integer で保存すると 2038-01-19T03:14:07Z 以降オーバーフロー。32bit 組み込みシステム (IoT、古い DB フォーマット) の時限爆弾。

対策: ほとんどの現代 OS/言語は 64bit に移行済みだが、レガシーな C/C++ バイナリ、一部 DB スキーマ、ファイルフォーマット を点検する必要がある。time_t を明示的にサイズ確認。

3.4 紀元 0 年問題 vs 負の epoch

-62135596800 は西暦 1 年 1 月 1 日 UTC。それ以前 (紀元前) は protocol により表現可能/不可能。Java の Instant.MIN は -9999999-01-01、Instant.MAX は 9999999-12-31。


4. ISO 8601 — 文字列表現の標準

4.1 基本形式

2026-04-15                       日付
2026-04-15T13:30:00              ローカル時刻
2026-04-15T13:30:00Z             UTC
2026-04-15T13:30:00+09:00        ソウル時刻
2026-04-15T13:30:00.123456789Z   ナノ秒精度
2026-W16-3                       週基準 (ISO 週 16 の水曜)
P1Y2M10DT2H30M                   期間 (1 年 2 ヶ月 10 日 2 時間 30 分)
2026-04-15/2026-04-20            区間

4.2 RFC 3339 — ISO 8601 のサブセット

IETF がインターネットプロトコル用に制限したサブセット。小文字 T 許可、タイムゾーンオフセット必須など。

4.3 日常の落とし穴

教訓: 文字列でやり取りする際は必ず タイムゾーンオフセットを明示。パーサは ISO 8601 公式ライブラリ (Java Instant.parse、Python 3.11+ fromisoformat、JS Temporal.Instant.from) を使う。


5. タイムゾーン — 政治の時間

5.1 タイムゾーンの役割

UTC からのオフセットを決定する。ただしオフセットは:

5.2 IANA Time Zone Database (tz database、zoneinfo、Olson DB)

1986 年から Paul Eggert が維持している タイムゾーン史の教科書Asia/SeoulAmerica/New_York のような名前をキーに、その地域が歴史的に使用したすべてのオフセット/DST ルール/変更履歴を含む。

例: Asia/Seoul は 1954 年 3 月 21 日に UTC+8:30 から UTC+9 へ変更。1961 年までに何度かオフセットが変わり、1987〜1988 年に DST があったが廃止。現在の韓国 API で「1955 年 1 月 1 日 00:00 Seoul」を UTC に変換するには、この歴史を知る必要がある。

5.3 tzdata 更新

毎年 5〜10 回新バージョンが出る。最近の変更:

運用上の重要点:

5.4 略語の曖昧さ

CST — どの国?

IST — さらに深刻:

ルール: 略語は表示用のみに使う。プログラムでは IANA 名 (Asia/SeoulAmerica/Chicago) を使え。

5.5 POSIX TZ 文字列

古い形式だが今も生きている: EST5EDT,M3.2.0,M11.1.0。絶対に使わず IANA 名を使え。


6. DST — 現場の地雷

6.1 「存在しない時刻」

2024 年 3 月 31 日のヨーロッパ、02:00 → 03:00 にジャンプ。02:30 は 存在しない

// JavaScript をよく知らない場合
new Date('2024-03-31T02:30:00+01:00').toISOString()
// "2024-03-31T01:30:00.000Z" — 実際には意図した 02:30 ではない

対策:

6.2 「2 回現れる時刻」

2024 年 10 月 27 日のヨーロッパ、03:00 → 02:00 に戻る。02:30 は 2 回 存在する。

6.3 カレンダー予約の惨事

「毎週月曜 09:00 ミーティング」を UTC に変換して保存したら:

元の civil+tz で保存 するのが正解。DB カラムを (timestamptimezonerecurrence_rule) に分離する。

6.4 DST 廃止の流れ

EU は 2019 年に「2021 年から DST 廃止」を決定したが各国の意見対立で延期。米国は 2022 年の「Sunshine Protection Act」で永久 DST 法案が上院通過するも下院保留。トルコ/ロシア/アイスランド/中国などは既に廃止。

開発者視点: 廃止の決定は 数ヶ月前に突然 確定するので、tzdata 更新チェーンを常に維持する必要がある。


7. 言語別の datetime API

7.1 JavaScript の Date — 悪名高い設計

1995 年に Netscape が Java の java.util.Date を急いでコピーしたが、その Java API は後に廃止された (Joda Time / java.time に置き換え)。しかし JS は 数十年間互換性のために直せなかった

問題点:

Moment.js、date-fns、Luxon、Day.js のようなライブラリがこのギャップを埋めた。

7.2 Temporal — 2025 年の救世主

Stage 3 → 2024 年末にブラウザ/Node に順次実装。主要型:

const now = Temporal.Now.zonedDateTimeISO('Asia/Seoul')
const oneHourLater = now.add({ hours: 1 })
const duration = now.until(oneHourLater) // Temporal.Duration

革新ポイント:

7.3 Python

from datetime import datetime
from zoneinfo import ZoneInfo
dt = datetime(2026, 4, 15, 13, 30, tzinfo=ZoneInfo("Asia/Seoul"))
dt.astimezone(ZoneInfo("America/New_York"))

7.4 Java

7.5 Go

7.6 Rust


8. Monotonic Clock vs Wall Clock

8.1 違い

8.2 なぜ重要か

経過時間計測 には必ず monotonic を使う:

// NG — NTP sync が戻ると負の値になる
const start = Date.now()
await doWork()
const elapsed = Date.now() - start

// OK
const start = performance.now()
await doWork()
const elapsed = performance.now() - start

タイムアウト / rate limiter / ベンチマーク はすべて monotonic clock を使う必要がある。

8.3 言語別の monotonic アクセス


9. NTP、PTP — 分散システムの時計同期

9.1 NTP (Network Time Protocol)

9.2 PTP (Precision Time Protocol、IEEE 1588)

9.3 Google TrueTime

Spanner DB が世界規模で一貫したトランザクションを行うために作ったシステム。時刻を単一値ではなく [earliest, latest] の区間 として扱う。

類似アイデア: CockroachDB の HLC (Hybrid Logical Clock) — NTP + 論理時計の組み合わせ。

9.4 実務の教訓


10. 暦体系 — グレゴリオ暦がすべてではない

10.1 主要な暦

10.2 Unicode CLDR / ICU

多言語暦のサポート:

new Intl.DateTimeFormat('ja-JP-u-ca-japanese', { year: 'numeric', month: 'long', day: 'numeric' })
  .format(new Date('2026-04-15'))
// "令和8年4月15日"

new Intl.DateTimeFormat('ko-KR-u-ca-buddhist').format(new Date('2026-04-15'))
// "불기 2569년"

10.3 閏年ルール (グレゴリオ暦)

2000 年は閏年 (400)、1900 年/2100 年は平年 (100)。

「毎年 2 月 29 日のミーティング」は 4 年に 1 度しか発生しない。Outlook などは「前月末日」または「翌月 1 日」の fallback オプションを提供。

10.4 1752 年 9 月 3〜13 日の空白

英国は 1752 年にユリウス暦からグレゴリオ暦に切り替えた際、9 月 2 日の次が 9 月 14 日。ロシアは 1918 年。この歴史的空白を 正確にモデル化するライブラリは稀 (Java GregorianCalendar、Noda Time などが一部サポート)。


11. データベースの時刻処理

11.1 PostgreSQL

推奨: 特別な理由がなければ timestamptz を使う。

11.2 MySQL

注意: TIMESTAMP はサーバの time_zone セッション変数によって結果が変わる。コネクションごとに SET time_zone = '+00:00' を推奨。

11.3 MongoDB

11.4 DynamoDB、Redis

11.5 ClickHouse


12. ログとメトリクスの timestamp

12.1 原則: UTC で保存、ローカルで表示

12.2 Correlation と Clock Skew

マイクロサービス A、B、C があり、A が B を呼び、B が C を呼ぶ。各サーバの時計が少しずつ異なると:

12.3 ISO 8601 または RFC 3339 で保存

2026-04-15T13:30:00.123456789Z

ここで Z (= UTC) は必須。tz なしの文字列はパースが曖昧になる。

12.4 Kafka など Event Stream

各メッセージは producer timestamp (producer が付けた時刻) と log append time (broker が保存した時刻) の 2 つを持つ。順序ソートは offset で行い、時刻は参考用。


13. 実務バグケース 10 選

13.1 「夜中 2 時に実行される cron が DST の日に実行されない」

原因: 2:30 が存在しないタイムゾーン。cron 式はローカル時刻基準。 解決: UTC で cron を回すか、OnCalendar=UTC オプションを明示。

13.2 「JWT exp が妙に早く期限切れになる」

原因: サーバとクライアントの時計差。 解決: 5〜30 秒の leeway。NTP 同期を強制。

13.3 「メールの 'Received' ヘッダの時刻が未来」

原因: どこかの中継サーバの時計が進んでいる。 解決: インフラチームに NTP 点検を依頼。Spam filter はこのケースを疑わしい信号として扱う。

13.4 「誕生日通知が 1 日早く/遅く来る」

原因: UTC 基準で保存したがユーザーが別の tz に移動。 解決: 誕生日は PlainDate で保存 (時刻なし)。通知送信時にユーザーの tz の 00:00 にマッピング。

13.5 「1970-01-01 と表示される」

原因: null timestamp を 0 として扱い Date で format。 解決: null チェック。

13.6 「2038 年のテストが失敗」

原因: 32bit time_t。 解決: 64bit コンパイルを確認。

13.7 「duration が負」

原因: wall clock を使用。NTP が後ろに補正。 解決: monotonic clock (performance.nowtime.monotonic)。

13.8 「イベントログの順序がバラバラ」

原因: サーバ間の clock skew。 解決: OpenTelemetry の Span Link + Logical Clock。

13.9 「ユーザーが『今日』予約したのに『明日』として保存される」

原因: ブラウザ tz ≠ サーバ tz。サーバで new Date(...) でパースするとサーバ tz 基準。 解決: ブラウザで Temporal.PlainDate.from(...) または ISO 文字列 + tz オフセット明示。

13.10 「休暇日数の計算が DST 切り替え時に 0.96 日」

原因: (endMs - startMs) / (24 * 3600 * 1000) で DST により 1 日が 23 または 25 時間になる。 解決: ChronoUnit.DAYS.between(zonedStart, zonedEnd) のような civil-aware な演算。


14. チェックリスト — 時刻関連システム設計

保存

変換

DST

同期

ライブラリ

運用


15. 歴史的事件アーカイブ

15.1 2007 年の米国 DST 日付変更

米政府が DST 開始を 3 週間前倒し。多くの Outlook/Blackberry が旧ルールで動き、ミーティングが 1 時間ずれた。MS が緊急パッチを配布。

15.2 2011 年のサモアのタイムゾーン変更

国際日付変更線を越えたため 2011 年 12 月 30 日が存在しなかった。12 月 29 日の次が 12 月 31 日。ニュージーランドの取引時間帯に合わせるため。

15.3 2016 年のトルコ

当日決定で DST を廃止。古い Android 端末が自動変更されず、1 時間遅れて出勤。

15.4 2018 年の北朝鮮

UTC+8:30 → UTC+9 に変更 (南北統一のジェスチャー)。tzdata 更新。

15.5 2023 年のグリニッジ標準時の死?

UK 政府が Brexit 後のタイムゾーン政策を議論。まだ変化なし。

15.6 金融の High-Frequency Trading

NASDAQ などは ナノ秒 精度で注文順序を記録。PTP + 共有 atomic clock。1 マイクロ秒の差が数百万ドルの取引結果を左右する。


結び — 「時間は複数ある」

本稿で繰り返した視点の 1 つ: 「時間」という単一概念は存在しない。Instant、Civil time、Zoned time、Monotonic、Logical time など複数の層の時間があり、それぞれ異なる目的に適している。プログラムのバグの多くはこれらの層を混同したときに発生する。

核心原則 5 つ:

  1. 保存は UTC、表示はローカル — そして繰り返しスケジュールは元の civil+tz を保持
  2. ソート・計算は Instant で、UI は ZonedDateTime で
  3. 経過時間は Monotonic で (wall clock はジャンプし得る)
  4. タイムゾーンは IANA 名で (略語禁止)
  5. DST 切り替え日をテストケースに含める

Unicode と同様、時間も 人間社会の複雑さ がソフトウェアに染み込んだ領域だ。政治的決定で DST が変わり、宗教暦によって祝日が変わり、地球の自転が遅くなって leap second が挿入される。「Just use UTC」では解決しない現実において、開発者は 時間のレイヤードモデルを頭の中に持つ 必要がある。

次回の記事では 分散システムの Consensus — Raft/Paxos、CAP 定理、そして eventual consistency を掘り下げる。本稿で見た 時間の不一致 が分散システムの核心的難題とどう絡むか (Lamport clock、Vector clock、Hybrid Logical Clock など) を続けて見ていこう。

時間を理解することは単にバグを避ける技術ではない。「同時に起きた」とは何を意味するのか という哲学的問いでもある。相対性理論が語るように、分散システムで「全世界共通の現在時刻」は存在しない — 我々はその近似を扱っているにすぎない。

コメント

まだコメントはありません。

ログインするとコメントできます