マガジン

日付が1日ずれる不具合:タイムゾーンが原因のバグをAI生成コードで直す

AIを使いこなす

Claudeで執筆し、GPTで自動校閲・修正しています。

紙のカレンダーで隣接する日付を指して確認している手元
記事の内容をもとに制作したAI生成イメージです。実際の製品画面や検証結果の写真ではありません。

AIが書いたJavaScriptで「3月15日のはずが3月14日と表示される」ときの原因は、多くの場合、日付だけの値を「ある瞬間」を表す Date に変換し、UTCの暦日と日本時間の暦日を取り違えていることです。日本時間(JST)はUTCより9時間進んでいるため、日本時間の0:00〜8:59は、UTCではまだ前日です。この9時間の帯に当たる日時を選んでテストすると、ずれを確実に再現できます。 この記事では、境界日時を並べたテストケース表、Date で注意する点、AIに修正を頼むときの依頼文を順に示します。 なお、表の数値は仕様から計算した「説明用の例」で、私がコードを実行して得た結果ではありません。お使いの環境で一度実行して確かめてください。

この記事の目次
  1. 原因:同じように見える文字列が、9時間違う瞬間になる
  2. テストケース表:UTCと日本時間の境界日時
  3. 再現するときの手順
  4. 判断表:その値は「日付だけ」か「瞬間」か
  5. Dateで注意する点
  6. AIに修正を頼む依頼文の例
  7. 直ったかの最終確認

原因:同じように見える文字列が、9時間違う瞬間になる

MDNのDateの解説によると、タイムゾーンのオフセットが付いていない文字列は、日付だけの形式はUTC、日時の形式はローカル時刻として解釈されます。MDNはこのUTC扱いについて、過去の仕様上の誤りが互換性のため直せなかったものだと説明しています(MDN: Date)。

つまり、見た目がほぼ同じでも次のように変わります。

  • new Date("2026-03-15")、解釈:日付だけ → UTC、指す瞬間(説明用の例):UTC 3/15 0:00(日本時間 3/15 9:00)
  • new Date("2026-03-15T00:00:00")、解釈:日時でオフセットなし → ローカル、指す瞬間(説明用の例):日本時間 3/15 0:00(UTC 3/14 15:00)
  • new Date(2026, 2, 15)、解釈:引数形式 → ローカル、指す瞬間(説明用の例):同上(月は0始まりなので2が3月)

引数を並べる形式がローカル時刻で評価されること、月が0始まりであることはMDN: Dateコンストラクターにも書かれています。

ずれが起きるのは、こうして作った Date を別のタイムゾーンの基準で読み出したときです。典型は次の2つです。

  • 日本時間で作った0:00の Date に toISOString().slice(0, 10) をかける。toISOString() は常にUTCで出力する(MDN: toISOString)ので、前日の日付が出ます。
  • UTCで動くサーバー上で getDate() を使い、日本のユーザーの「今日」を決める。日本時間の朝9時前は前日になります。

日本にいる開発者の手元(JST)で new Date("2026-03-15").getDate() が15になって気づきにくい点も厄介です。UTCより西の地域(米国など)で動かすと、同じコードが14を返します。

テストケース表:UTCと日本時間の境界日時

使う日時は、日付が切り替わる前後を選びます。下の表は、同じ瞬間をUTCの暦日と日本時間の暦日で読んだときの差を示します(説明用の例)。右端の列は「toISOString().slice(0, 10) の結果」で、UTC環境の getDate() で取った日付とも一致します。

  • 1、日本時間:2026-03-15 00:00:00、UTC(Z付きで入力):2026-03-14T15:00:00Z、期待する日本の日付:2026-03-15、UTCの暦日:2026-03-14、一致:×ずれる
  • 2、日本時間:2026-03-15 08:59:59、UTC(Z付きで入力):2026-03-14T23:59:59Z、期待する日本の日付:2026-03-15、UTCの暦日:2026-03-14、一致:×ずれる
  • 3、日本時間:2026-03-15 09:00:00、UTC(Z付きで入力):2026-03-15T00:00:00Z、期待する日本の日付:2026-03-15、UTCの暦日:2026-03-15、一致:○
  • 4、日本時間:2026-03-15 23:59:59、UTC(Z付きで入力):2026-03-15T14:59:59Z、期待する日本の日付:2026-03-15、UTCの暦日:2026-03-15、一致:○
  • 5、日本時間:2026-03-16 00:00:00、UTC(Z付きで入力):2026-03-15T15:00:00Z、期待する日本の日付:2026-03-16、UTCの暦日:2026-03-15、一致:×ずれる
  • 6、日本時間:2026-03-01 00:00:00、UTC(Z付きで入力):2026-02-28T15:00:00Z、期待する日本の日付:2026-03-01、UTCの暦日:2026-02-28、一致:×月がずれる
  • 7、日本時間:2027-01-01 00:00:00、UTC(Z付きで入力):2026-12-31T15:00:00Z、期待する日本の日付:2027-01-01、UTCの暦日:2026-12-31、一致:×年がずれる

見るべき点は3つです。

  • #2と#3が境界の組です。1秒違いで結果が変わるので、判定が < か <= かの確認にも使えます。
  • #3と#4は「ずれない」対照です。昼間だけで試すと、バグが見つかりません。
  • #6と#7は月末・年末の事故用です。月次集計や年度の切り替えで起きるため、必ず入れます。

日本は夏時間を採用していないため、日本時間側は常に+9時間として扱えます。他の地域の日付も扱う場合は、この表だけでは足りません。

確認用の短いコード

次の例は実行環境のタイムゾーンに依存しない書き方です(説明用の例・未実行)。

js
const JST_MS = 9 * 60 * 60 * 1000;

// 修正版: 瞬間(Date)を日本時間の "YYYY-MM-DD" にする
function toJstYmd(date) {
  return new Date(date.getTime() + JST_MS).toISOString().slice(0, 10);
}

// 比較用: ずれやすい書き方(UTCの暦日になる)
const naive = (d) => d.toISOString().slice(0, 10);

const cases = [
  ["2026-03-14T15:00:00Z", "2026-03-15"],
  ["2026-03-14T23:59:59Z", "2026-03-15"],
  ["2026-03-15T00:00:00Z", "2026-03-15"],
  ["2026-03-15T14:59:59Z", "2026-03-15"],
  ["2026-03-15T15:00:00Z", "2026-03-16"],
  ["2026-02-28T15:00:00Z", "2026-03-01"],
  ["2026-12-31T15:00:00Z", "2027-01-01"],
];

for (const [iso, expected] of cases) {
  const d = new Date(iso);
  const fixed = toJstYmd(d);
  console.log(
    iso, "期待", expected,
    "修正版", fixed, fixed === expected ? "OK" : "NG",
    "素朴版", naive(d)
  );
}

修正版は、9時間足した値をUTC基準で読むことで、日本時間の暦日を取り出しています。日本時間専用の簡易的な方法なので、複数の地域を扱うアプリでは別の設計が必要です。

再現するときの手順

  1. 手元のタイムゾーンを確認します。console.log(new Date().getTimezoneOffset()) が日本時間なら -540 になります。getTimezoneOffset() は現在のロケールのオフセットを分で返す関数で、夏時間などで時点により値が変わり得る、とMDNに説明があります。
  2. 実行環境のタイムゾーンを変えて同じテストを走らせます。Node.jsではTZ環境変数を指定して実行できますが、指定後に上のオフセットが想定通りか、必ず出力して確かめてください。
  3. AIが書いた関数に、表の#1〜#7を入力して期待値と比べます。#1、#2、#5〜#7が落ちれば、UTCの暦日と日本の暦日の取り違えが原因です。
  4. 落ちた行の呼び出し元を見て、下の判断表で扱いを決めます。

判断表:その値は「日付だけ」か「瞬間」か

直し方は、その値が何を表すかで決まります。AIには、これを先に決めて伝えると直しが安定します。

  • 誕生日、締切日、祝日、予約の「日」、性質:日付だけ(時刻もタイムゾーンもない)、推奨する扱い:"2026-03-15" の文字列のまま保持し、Date に変換しない。日数計算が必要なときだけ、年・月・日の数値に分けて扱う
  • 作成日時、ログ、決済時刻、性質:ある瞬間、推奨する扱い:UTCの Z 付きISO文字列か、+09:00 付きで保存・受け渡しする
  • 画面表示用の「日本の日付」、性質:瞬間から導く表示値、推奨する扱い:表示直前に、日本時間として暦日へ変換する(上の toJstYmd など)
  • 「2026/03/15」のような独自書式の入力、性質:形式不明、推奨する扱い:Date に直接渡さず、自前で年・月・日に分解する

最後の行の理由は、MDNが、標準形式に合わない文字列の解釈は実装に任されており、多くの実装ではローカルのタイムゾーンを使うと述べているためです。ブラウザやランタイムによって結果が違い得ます。

Dateで注意する点

  • オフセットなしの文字列に注意する。 "2026-03-15" はUTC、"2026-03-15T00:00:00" はローカルです。時刻つきの値は Z か +09:00 を必ず付けると、瞬間が一意に決まります。
  • toISOString() は常にUTC。 日本の日付が欲しいときに slice(0, 10) を使うと、日本時間の0:00〜8:59にずれます。
  • getDate() などの読み出しは実行環境のタイムゾーン依存。 サーバーとブラウザで結果が変わる原因になります。getUTCDate() などのUTC版は環境に依存しません。
  • 月は0始まり。 new Date(2026, 2, 15) は3月15日です。AIが月を1つずらして書く事故も、境界テスト(#6)で見つかります。
  • 日付だけの値を Date にしない設計が最も安全。 変換しなければ、ずれる余地が生じません。

AIに修正を頼む依頼文の例

次の文面は、そのまま貼って使える形です。【】 の部分を置き換えてください。機密性のある実データではなく、説明用の日付に置き換えて渡します。

次のJavaScript関数で、日本のユーザーに対して日付が1日ずれる不具合があります。
原因がタイムゾーン(UTCと日本時間の差)かどうかを調べて、修正してください。

【関数のコードを貼る】

前提:
- 対象は日本のユーザー(日本時間、夏時間なし)。サーバーはUTCで動く場合がある。
- この関数の入力は【日付だけの値 / ある瞬間 のどちらか】。
- 日付だけの値は Date に変換せず、"YYYY-MM-DD" のまま扱いたい。

お願い:
1. ずれる原因となっている行を指摘し、なぜずれるかを短く説明する。
2. 修正後のコードを出す。
3. 次の境界日時を入力とするテストを書く(UTC表記で、期待値は日本の日付)。
   2026-03-14T15:00:00Z → 2026-03-15
   2026-03-14T23:59:59Z → 2026-03-15
   2026-03-15T00:00:00Z → 2026-03-15
   2026-03-15T15:00:00Z → 2026-03-16
   2026-02-28T15:00:00Z → 2026-03-01
   2026-12-31T15:00:00Z → 2027-01-01
4. テストは実行環境のタイムゾーンに依存しないこと。実行していない場合は「未実行」と明記する。

AIが返した修正は、そのまま信用せず、表の7行を自分の環境で通してから取り込みます。AIは「テストは通ります」と書くことがありますが、実行した結果かどうかは依頼文の4で区別させ、通ったかは自分の画面で確かめるのが確実です。

直ったかの最終確認

次の3点を確認できれば、今回の問いに対する修正としては十分です。

  • 表の#1、#2、#5、#6、#7が、期待する日本の日付になる。
  • 対照の#3、#4が、修正前後で変わらない。
  • 修正後のコードに、日付だけの文字列を new Date("YYYY-MM-DD") で変換し、toISOString() や getDate() で読み戻す処理が残っていない。

残っていれば、その行が次のずれの原因になります。

出典・参考資料