
タイムゾーンと日付の正しい扱い方 - UTC・ISO 8601・夏時間の罠と JavaScript Date
「1日ずれる」「サーバーとローカルで時刻が違う」「夏時間で予定が壊れる」——日付とタイムゾーンは、地味に事故が多い領域です。原因の多くはUTC・オフセット・タイムゾーンの混同と JavaScript の Date の癖にあります。この記事では、MDN・RFC 3339・IANA を一次ソースに、正しい扱い方を整理します。
まず用語を分ける
| 用語 | 例 | 性質 |
|---|---|---|
| UTC | 協定世界時 | 世界の基準(オフセット±0の参照点) |
| UTCオフセット | +09:00 | 固定の差分。DSTで変わりうる |
| IANAタイムゾーン | Asia/Tokyo | 地域+規則の集合。DSTルールを内包 |
| Unixエポック | 1970-01-01T00:00:00Z | 時刻の基点 |
いちばん大事なのが オフセットとタイムゾーンの違いです。+09:00 は「今この瞬間の差分」を表すだけで、将来/過去の夏時間の切り替えは追えません。America/New_York のような IANA識別子なら、過去から未来までのルール(DSTを含む)を tz データベースで正しく扱えます。
NOTE
オフセット(-05:00)だけで保存すると、夏時間の切り替えに対応できません。「いつ・どの地域か」を保つなら IANA 識別子(Asia/Tokyo 等)で扱います。tz データは OS や DB が採用する IANA Time Zone Database が出どころで、各国の制度変更で随時更新されます。
フォーマットは ISO 8601 / RFC 3339
時刻を文字列で持つなら、ISO 8601(インターネット向けに厳密化した RFC 3339)が基本です。
2026-06-23T12:34:56+09:00 # オフセット付き
2026-06-23T03:34:56Z # UTC(Z = +00:00)- 日付と時刻は
Tで区切る、年は4桁、オフセットかZを付ける - RFC 3339 では
-00:00に「UTCだがローカルTZは不明」という特別な意味がある(Z/+00:00とはニュアンスが異なる)
JavaScript Date のハマりどころ
Date は罠が多いので、代表例を押さえます(いずれも MDN に記載)。
月が0始まり
new Date(2026, 0, 1); // 1月1日(0 = January)
new Date(2026, 11, 31); // 12月31日(11 = December)文字列パースの非対称性(最重要)
// 日付のみの文字列は UTC として解釈される(歴史的な仕様の誤り)
new Date("2026-06-23"); // UTC 0時 → JSTでは 06-23 09:00 と表示
// 日時(オフセットなし)はローカル時刻として解釈される
new Date("2026-06-23T12:00:00"); // JSTでは 06-23 12:00(+09:00)同じ「日付っぽい文字列」でもUTC解釈とローカル解釈で食い違うのが「1日ずれる」の典型原因です。
getTimezoneOffset の符号が逆
new Date().getTimezoneOffset(); // JST環境で -540(正がUTC-、負がUTC+)その他
- 非標準文字列のパースは実装依存(
new Date("June 23, 2026")はブラウザ差あり・非推奨) getFullYear/getMonthなどはローカルTZ依存。UTCで取りたいならgetUTCFullYear等を使い分ける
夏時間(DST)の罠
DSTのある地域では、切り替え時に2種類の異常な時刻が生まれます。
- ギャップ(存在しない時刻): 春に時計を進める瞬間。例: ニューヨークで
2024-03-10T02:30は存在しない - フォールド(重複する時刻): 秋に時計を戻す瞬間。例: ニューヨークで
2024-11-03T01:30は EDTとESTで2回出現する
// America/New_York
new Date("2024-02-01").getTimezoneOffset(); // 300 (UTC-5, EST)
new Date("2024-08-01").getTimezoneOffset(); // 240 (UTC-4, EDT)「オフセットは固定」と思い込むと、DST境界で1時間ずれる/例外になるバグを踏みます。
新標準: Temporal
Date の設計上の問題(タイムスタンプと日時の二面性、任意IANA TZを直接扱えない、ミュータブル等)を解決するのが Temporal です。
| クラス | 用途 |
|---|---|
Temporal.Instant | 絶対時刻(TZなし) |
Temporal.PlainDate | 日付のみ(誕生日など) |
Temporal.ZonedDateTime | 日時+IANA TZ(予定・国際対応) |
ギャップ/フォールドの曖昧さも disambiguation(compatible/earlier/later/reject)で明示的に扱えます。2026年6月時点ではブラウザ対応は限定的(Baseline未到達)で、当面はポリフィル併用が現実的です。詳細はNode.js 26 と Temporalも参照。
ベストプラクティス
- 保存・転送はUTC(またはISO 8601+オフセット)。DB・API は統一する
- 表示時にだけユーザーのTZへ変換。変換は IANA識別子で
new Date("YYYY-MM-DD")のUTC解釈に注意。ローカル日付の意図なら日時形式かライブラリで- 非標準文字列をパースしない。標準フォーマットか専用ライブラリで
- サーバーのシステムTZに依存しない(環境変数
TZに頼らない) - 誕生日・記念日など「時刻なしの日付」は
Dateで持たない。YYYY-MM-DD文字列かTemporal.PlainDateで - 表示は
Intl.DateTimeFormat、計算は tzデータを持つライブラリ(Luxon / date-fns-tz)か Temporal
まとめ
- オフセット(
+09:00)とIANAタイムゾーン(Asia/Tokyo)は別物。DST対応にはIANA識別子 - 文字列は ISO 8601 / RFC 3339(
T区切り・4桁年・オフセットかZ) Dateは月0始まり・日付のみ文字列はUTC解釈・getTimezoneOffsetが逆符号に注意- DSTは存在しない時刻(ギャップ)と重複する時刻(フォールド)を生む
- 鉄則は 「保存はUTC・表示時に変換・IANAで扱う」。新規は Temporal も視野に
日付バグの多くは「どのタイムゾーンの、いつの時刻か」が曖昧なまま処理することで起きます。UTCで持ち、表示の瞬間だけ変換する——この一線を守るだけで、ほとんどのズレは防げます。


