0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

`toISOString().slice(0,10)` で日付が1日前になった — Date が持っているのは日付ではなく「瞬間」

0
Last updated at Posted at 2026-08-16

日本時間の深夜0時に作った Date を、いつもの書き方で日付文字列にしたら、1日前の日付が出てきた。

同じ瞬間なのに、日本と世界標準時では日付が1日ちがう(筆者作成)

const d = new Date(2026, 7, 18, 0, 30); // JST 2026-08-18 00:30
d.toISOString().slice(0, 10);           // "2026-08-17"  ← 1日前
d.getDate();                            // 18            ← こっちは合ってる

同じ Date なのに、聞き方で答えが割れる。バグでも環境の不具合でもなく、Date の設計どおりの挙動だ。この記事は、なぜ割れるのか、日本では1日のうち何時間が外れるのか、どう書けば直るのかを、実行結果つきで置いていく。

対象読者

  • フロントで日付を組み立ててサーバに送っている人
  • input[type=date] の値を new Date() に渡している人
  • 日次集計のキー(「2026-08-17」のような文字列)をJavaScript側で作っている人

このどれかに当てはまるなら、たぶん同じ地雷を踏んでいるか、これから踏む。検証環境は Node.js v26.0.0、macOS、タイムゾーンは Asia/Tokyo

原因。Date が持っているのは日付ではなく「瞬間」

Date オブジェクトは「2026年8月17日」という日付を持っていない。持っているのは 1970年1月1日 0:00 UTC からの経過ミリ秒、つまり地球上のどこでも同じ1つの瞬間だ。「何日か」は、その瞬間をどのタイムゾーンで読むかで変わる。

const d = new Date(2026, 7, 18, 0, 30); // ローカル(JST)の 8/18 0:30 という瞬間を作る
d.toString();      // "Tue Aug 18 2026 00:30:00 GMT+0900"  ← ローカルで読んだ
d.toISOString();   // "2026-08-17T15:30:00.000Z"           ← UTCで読んだ

toISOString()必ずUTCで読むメソッドで、JSTはUTCより9時間進んでいるから、JSTの0:30はUTCではまだ前日の15:30。だから slice(0, 10) で頭10文字を切ると前日の日付が出る。getDate() はローカルで読むので18が返る。同じ瞬間を違うゾーンで読んでいるだけで、どちらも正しい。

実測。日本では1日のうち9時間が外れる

JSTの0:00〜8:59に作った Date は、UTCではすべて前日になる。24時間ぶん回して数えた。

let bad = 0;
for (let h = 0; h < 24; h++) {
  const x = new Date(2026, 7, 18, h);
  if (x.toISOString().slice(0, 10) !== "2026-08-18") bad++;
}
console.log(bad, "/ 24");   // 9 / 24  → 37.5%

1日の37.5%が外れる。しかも外れるのが深夜〜朝なので、日中の開発中は再現せず、夜間バッチや早朝の入力で本番だけズレる、という見つけにくい出方をする。

文字列を渡すときも同じ罠がある

new Date("...") に文字列を渡すとき、区切り文字で解釈の基準が変わる。

new Date("2026-08-17");   // Mon Aug 17 2026 09:00:00 GMT+0900  ← UTCの0:00として解釈(JSTでは9時)
new Date("2026/08/17");   // Mon Aug 17 2026 00:00:00 GMT+0900  ← ローカルの0:00として解釈

ハイフン区切りの日付だけの文字列(ISO形式の日付のみ)は仕様上UTCとして解釈される。input[type=date] の値はまさにこの 2026-08-17 形式なので、そのまま new Date() に渡すとJSTでは朝9時の瞬間になる。そこから getDate() する分には合うが、時刻まで使う処理や、他の日付と引き算する処理で9時間ぶんズレる。

直し方

「今日の日付の文字列」が欲しいだけなら

toISOString().slice(0,10)toLocaleDateString("sv-SE") に置き換える。スウェーデンのロケールは日付を YYYY-MM-DD で出すので、書式がISOと同じで、かつローカルのタイムゾーンで読んでくれる。

d.toLocaleDateString("sv-SE");   // "2026-08-18"

サーバがUTCで動いているとき

サーバ側のNodeでは「ローカル」がUTCなので、sv-SE だけだとまたUTC基準になる。ゾーンを明示する。

d.toLocaleDateString("sv-SE", { timeZone: "Asia/Tokyo" });   // "2026-08-18"

どこで動いても同じ答えにしたければ、こちらを常用する方が安全だ。

input[type=date] の値を受け取るとき

文字列のまま扱えるならDateにしない。時刻計算が必要なら、UTC解釈を避けて分解して作る。

const [y, m, day] = "2026-08-17".split("-").map(Number);
const d = new Date(y, m - 1, day);   // ローカルの0:00

まとめ

  • Date は日付を持っていない。UTC基準の「瞬間」を1つ持っている
  • toISOString() は必ずUTCで読む。JSTでは0:00〜8:59の9時間(37.5%)が前日になる
  • 日付文字列が欲しいなら toLocaleDateString("sv-SE", { timeZone: "Asia/Tokyo" })
  • new Date("2026-08-17") はUTC解釈、new Date("2026/08/17") はローカル解釈

この記事の限界

  • 検証はNode v26 + macOSのみ。ブラウザ実装でも toISOString の仕様は同じだが、toLocaleDateString の出力はICUデータに依存するので、古い環境では sv-SE の書式が違う可能性がある
  • 夏時間(DST)のあるタイムゾーンでは、境界日にさらに1時間ぶんの罠がある。JSTにはDSTが無いので今回は触れていない
  • 新しい標準API Temporal を使えば「日付だけ」の型(PlainDate)があってこの問題自体が消えるが、対応環境がまだ限られる

自分の環境で確かめるなら、最初の3行を貼って実行するだけでいい。0時台に動かすと即座に再現する。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?