デジタル道具屋
デジタル道具屋
記事一覧へ戻る
更新: 読了 約 5 分

「日付が1日ズレる」の正体 - タイムゾーンの落とし穴と実務での扱い方

#development

執筆: ありがっさまりょうた

デジタル道具屋を個人で開発・運用しています。記事は実装時の検証結果と公式仕様を照合して執筆しています。

本記事の執筆方針と編集ポリシーについてはAbout ページをご覧ください。

導入:集計結果が1日ズレる典型的な原因

「海外のユーザー向けの売上レポートが日本時間で集計されており、月初の1日ぶんが異なる」という不具合は、タイムゾーン処理の典型的な失敗例です。 原因の多くは、バックエンドで日時を YYYY-MM-DD のような日付のみの文字列として保存していたか、 JST に設定されたサーバー上で new Date() を実行し、その結果を日付単位に丸めていたことにあります。

タイムゾーンを意識せずに日付文字列を切り出すと、UTC では 23:30 でも JST では翌日の 08:30 になる、といった日付境界をまたぐ時刻で必ず食い違いが発生します。 この記事では、その仕組みと回避のための設計ルールを整理します。

1. UTC、JST、ローカル時刻の関係を整理

まず用語を整理します。

必ず押さえる4つの用語
  • UTC:協定世界時。世界標準。サーバー保存の事実上のデファクト
  • JST:日本標準時。常に UTC+9(日本では夏時間が無い)
  • ローカル時刻:実行環境(OS / ブラウザ)が認識しているタイムゾーンでの時刻
  • DST(Daylight Saving Time):夏時間。米国の多くの州や欧州では年に2回切り替え。日本には無い

「日本に住んでるユーザー向けだから JST 固定でいい」と決めても、ユーザーが海外旅行中に予約を取るケースなどで容易に破綻します。 原則は「保存は UTC、表示時にユーザーのタイムゾーンへ変換」です。

2. JavaScript で日付がズレる典型パターン

パターン1:new Date('2026-05-08') は UTC として解釈される

文字列で日付を指定した場合、常にローカル時刻として解釈されるわけではありません。 ECMAScript の仕様(Date Time String Format、ISO 8601 の拡張形式)では、YYYY-MM-DD 形式(時刻なし)は UTC として扱われます。 一方で 2026-05-08T00:00:00 のように時刻を含み、かつタイムゾーン指定(Z や +09:00)が無い形式はローカル時刻と解釈されます。 この差で、JST 環境では1日ズレる事故が頻発します。

// JST 環境(Asia/Tokyo)
new Date('2026-05-08').toString()
// → "Fri May 08 2026 09:00:00 GMT+0900 (JST)"
// 内部的には UTC 2026-05-08 00:00 なので、JST では 09:00

new Date('2026-05-08T00:00:00').toString()
// → "Fri May 08 2026 00:00:00 GMT+0900 (JST)"
// 時刻付き・タイムゾーン指定なしはローカル時刻として 0:00

同じ日付を指すつもりの2つの文字列が、UTC に直すとまったく別の瞬間になることは toISOString() で確認できます。 次の例では日付のみの形式と時刻付きの形式で、UTC の日付が1日異なります。

// JST 環境(Asia/Tokyo)
new Date('2026-03-08').toISOString()
// → "2026-03-08T00:00:00.000Z"(UTC の 3/8 0:00 として解釈)

new Date('2026-03-08T00:00').toISOString()
// → "2026-03-07T15:00:00.000Z"(JST の 3/8 0:00 = UTC の 3/7 15:00)

なお 2026/05/08 のようなスラッシュ区切りは ECMAScript の仕様で定義された形式ではなく、解釈は実装依存です。 主要なブラウザや Node.js(V8)ではローカル時刻として扱われますが、環境をまたいで同じ結果になる保証はないため、仕様で定義された形式を使うのが安全です。

パターン2:toISOString() は常に UTC を返す

JST 環境で new Date('2026-05-08T08:00:00+09:00').toISOString() は '2026-05-07T23:00:00.000Z' となり、日付部分が前日になります。 これを .split('T')[0] で日付だけ取り出すと、5/8 のはずが 5/7 として保存されてしまいます。

ローカル時刻の日付だけを YYYY-MM-DD 形式で取り出したい場合は、toLocaleDateString('sv-SE') が利用できます。 スウェーデン語ロケールの日付表記が ISO 8601 と同じ YYYY-MM-DD であることを利用した方法で、主要なブラウザおよび Node.js で同じ結果が得られます。

// JST 環境(Asia/Tokyo)
const d = new Date('2026-05-08T08:00:00+09:00');

d.toISOString().split('T')[0]
// → "2026-05-07"(UTC の日付。意図とズレる)

d.toLocaleDateString('sv-SE')
// → "2026-05-08"(ローカル時刻の日付)

// 表示先のタイムゾーンを明示する場合
d.toLocaleDateString('sv-SE', { timeZone: 'America/New_York' })
// → "2026-05-07"(ニューヨークでは 5/7 19:00 EDT)

3. 実務で守るべき設計ルール

API・DB 設計の基本ルール
  1. サーバー・DB には UTC で保存:TIMESTAMP WITH TIME ZONE や DATETIME(UTC)
  2. API のレスポンスは ISO 8601 + Z 表記:2026-05-08T12:34:56.789Z
  3. ユーザーのタイムゾーンはプロフィールに保存:Asia/Tokyo のような IANA 名で
  4. 表示はユーザータイムゾーンで:Intl.DateTimeFormat や date-fns-tz を活用
  5. 「日付だけ」のフィールドは特に注意:誕生日や祝日は UTC ではなくローカル日付として扱う設計に

サマータイム(DST)が刺さるケース

米国・欧州の DST 切り替え当日は、ローカル時刻が「1時間飛ぶ」または「同じ時刻が2回ある」事態になります。 例えば米国西海岸では 3月 第2 日曜の 02:00 が突然 03:00 になります。 毎日 02:30 にバッチを動かすシステムは、その日だけバッチが「存在しない時刻」になり実行されません。 バッチ系は 必ず UTC で時刻を指定するのが鉄則です。

2026年の米国の DST 開始日は 3月8日(第2日曜)です。この日、America/Los_Angeles では 01:59:59 PST の次の秒が 03:00:00 PDT になり、02:00〜02:59 のローカル時刻は存在しません。 同じ日に Asia/Tokyo では何も起こらないため、日本のタイムゾーンでテストしているだけでは、この種の不具合は検出できません。

また、タイムゾーンのルール自体も固定ではありません。IANA Time Zone Database は年に数回更新されており、 たとえばトルコは2016年に DST を廃止して通年 UTC+3 に移行し、ロシアも2011年と2014年にタイムゾーンの定義を変更しています。 実行環境の tz データベースが古いと、こうした地域の時刻変換が誤った結果になるため、OS やランタイム、ライブラリ(tzdata パッケージなど)の更新も運用上の考慮事項です。

4. 試しながら理解する

「いま UTC で何時?」「東京と NY とロンドンの時差はいくつ?」を即座に確認するなら、当サイトのタイムゾーン変換ツールが便利です。 打ち合わせの設定や、ログのタイムスタンプを別ロケールで読み解くときに使ってください。

関連ツール: UNIX 時間変換、日数計算、年齢計算。

よくある失敗パターン

日本国内向けに開発・リリースしたサービスを海外展開する段階で、 「全 API レスポンスの日時が JST 前提のタイムゾーン指定なし文字列だった」と判明するケースがあります。 この状態から修正するには、すべての API のクライアント側・サーバー側の両方で解釈を変える必要があり、影響範囲が広くなります。 サービス開始時から ISO 8601 + Z 表記で統一しておけば、後からの追加コストはほぼゼロです。

参考にした一次情報

本記事の内容は、以下の公式仕様や一次情報を参照して執筆しています。

この記事の内容を実際に試す

解説した内容は、以下のツールでブラウザ上から無料で試せます。

タイムゾーン変換

世界各国の時間を比較・変換

UNIX時間変換

UNIXタイムスタンプと日時の相互変換

日数計算

2つの日付の間の日数・週数を計算

年齢計算

生年月日から現在の正確な年齢を計算