タイムゾーンと日付の正しい扱い方 - UTC・ISO 8601・夏時間の罠と JavaScript Date

タイムゾーンと日付の正しい扱い方 - UTC・ISO 8601・夏時間の罠と JavaScript Date

作成日:
読了:9
更新日:

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始まり

月は 0〜11
new Date(2026, 0, 1);   // 1月1日(0 = January)
new Date(2026, 11, 31); // 12月31日(11 = December)

文字列パースの非対称性(最重要)

日付のみ=UTC、日時=ローカル
// 日付のみの文字列は 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 の符号が逆

UTC+9 なのに -540
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:30EDTと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(予定・国際対応)

ギャップ/フォールドの曖昧さも disambiguationcompatible/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 3339T区切り・4桁年・オフセットかZ
  • Date月0始まり・日付のみ文字列はUTC解釈・getTimezoneOffsetが逆符号に注意
  • DSTは存在しない時刻(ギャップ)と重複する時刻(フォールド)を生む
  • 鉄則は 「保存はUTC・表示時に変換・IANAで扱う」。新規は Temporal も視野に

日付バグの多くは「どのタイムゾーンの、いつの時刻か」が曖昧なまま処理することで起きます。UTCで持ち、表示の瞬間だけ変換する——この一線を守るだけで、ほとんどのズレは防げます。

参考リンク

Astro 7 リリース - コンパイラもMarkdownもRust化、Vite 8でビルドが最大61%速く

Astro 7 リリース - コンパイラもMarkdownもRust化、Vite 8でビルドが最大61%速く

7

Astro 7 が2026年6月22日にリリースされました。.astro コンパイラを Go から Rust へ書き換え、Markdown/MDX 処理系に Rust 製の Sätteri を採用(デフォルト化)、バンドラは Vite 8(Rust 製 Rolldown)。ビルドは公式報告で15〜61%高速化。Queued Rendering の安定化、src/fetch.ts による高度ルーティング、Route Caching、AI エージェント検出付きのバックグラウンド開発サーバーなどの新機能と、HTML 厳格化や Sätteri デフォルト化(remark/rehype は明示オプトイン)といった6→7の破壊的変更を、Astro 公式を一次ソースに整理します。