「日付が1日ズレる」の正体 - タイムゾーンの落とし穴と実務での扱い方
執筆: ありがっさまりょうた
デジタル道具屋を個人で開発・運用しています。記事は実装時の検証結果と公式仕様を照合して執筆しています。
本記事の執筆方針と編集ポリシーについてはAbout ページをご覧ください。
導入:集計結果が1日ズレる典型的な原因
「海外のユーザー向けの売上レポートが日本時間で集計されており、月初の1日ぶんが異なる」という不具合は、タイムゾーン処理の典型的な失敗例です。 原因の多くは、バックエンドで日時を YYYY-MM-DD のような日付のみの文字列として保存していたか、 JST に設定されたサーバー上で new Date() を実行し、その結果を日付単位に丸めていたことにあります。
タイムゾーンを意識せずに日付文字列を切り出すと、UTC では 23:30 でも JST では翌日の 08:30 になる、といった日付境界をまたぐ時刻で必ず食い違いが発生します。 この記事では、その仕組みと回避のための設計ルールを整理します。
1. UTC、JST、ローカル時刻の関係を整理
まず用語を整理します。
- 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. 実務で守るべき設計ルール
- サーバー・DB には UTC で保存:
TIMESTAMP WITH TIME ZONEやDATETIME(UTC) - API のレスポンスは ISO 8601 + Z 表記:
2026-05-08T12:34:56.789Z - ユーザーのタイムゾーンはプロフィールに保存:
Asia/Tokyoのような IANA 名で - 表示はユーザータイムゾーンで:
Intl.DateTimeFormatやdate-fns-tzを活用 - 「日付だけ」のフィールドは特に注意:誕生日や祝日は 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 とロンドンの時差はいくつ?」を即座に確認するなら、当サイトのタイムゾーン変換ツールが便利です。 打ち合わせの設定や、ログのタイムスタンプを別ロケールで読み解くときに使ってください。
よくある失敗パターン
日本国内向けに開発・リリースしたサービスを海外展開する段階で、 「全 API レスポンスの日時が JST 前提のタイムゾーン指定なし文字列だった」と判明するケースがあります。 この状態から修正するには、すべての API のクライアント側・サーバー側の両方で解釈を変える必要があり、影響範囲が広くなります。 サービス開始時から ISO 8601 + Z 表記で統一しておけば、後からの追加コストはほぼゼロです。
参考にした一次情報
本記事の内容は、以下の公式仕様や一次情報を参照して執筆しています。
- RFC 3339 - Date and Time on the Internet: Timestamps
IETF
https://www.rfc-editor.org/rfc/rfc3339
- IANA Time Zone Database
IANA
https://www.iana.org/time-zones
- ECMAScript Date Time String Format
ECMA TC39
https://tc39.es/ecma262/#sec-date-time-string-format
この記事の内容を実際に試す
解説した内容は、以下のツールでブラウザ上から無料で試せます。