new Date('2026-10-30')はUTC、T00:00はローカルになる理由
new Date('2026-10-30').getDate() が、ニューヨークのタイムゾーンでは 29 を返す。日本で動かすと 30 なので気づきにくいが、海外のユーザーやタイムゾーンが UTC の CI サーバーでは日付が1日ずれる。
原因は、ECMAScript が「日付だけの文字列は UTC」「日付と時刻の文字列はローカル時刻」と決めていることにある。この記事では仕様の該当箇所を確認し、同じ日付を11通りの書き方で new Date に渡して、東京・ニューヨーク・UTC の3つのタイムゾーンで結果を比べる。
仕様: オフセットが無いとき、日付だけは UTC
ECMAScript 仕様の 21.4.1.32 Date Time String Format は、ISO 8601 の拡張形式をもとにした YYYY-MM-DDTHH:mm:ss.sssZ を定めている。この形式には YYYY / YYYY-MM / YYYY-MM-DD の「日付だけ」の形と、それに THH:mm などの時刻を続けた「日付と時刻」の形がある。
同じ節に、オフセット(Z や +09:00)が無いときの扱いが書かれている。
When the UTC offset representation is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time.
つまり 2026-10-30 は UTC の0時、2026-10-30T00:00 は実行環境のタイムゾーンの0時になる。MDN の Date.parse() のページも、日付だけの形は UTC、時刻を含みタイムゾーンの無い形はローカル時刻になる、という同じ説明をしている。
一方、21.4.3.2 Date.parse ( string ) は、文字列がこの形式に合わない場合は実装固有のヒューリスティックや形式にフォールバックしてよい、としている。2026/10/30 や 2026-10-30 00:00(T の代わりに空白)は形式外なので、どう読まれるかは仕様では決まっていない。
11通りの書き方を3つのタイムゾーンで実測
Node.js v24.11.1(Windows 11)で、次の文字列を new Date に渡し、toISOString()(UTC 表記)で比べた。
| 入力 | Asia/Tokyo | America/New_York | UTC |
|---|---|---|---|
2026-10-30 |
2026-10-30T00:00Z |
2026-10-30T00:00Z |
2026-10-30T00:00Z |
2026-10-30T00:00 |
2026-10-29T15:00Z |
2026-10-30T04:00Z |
2026-10-30T00:00Z |
2026-10-30T00:00:00 |
2026-10-29T15:00Z |
2026-10-30T04:00Z |
2026-10-30T00:00Z |
2026-10-30T00:00Z |
2026-10-30T00:00Z |
2026-10-30T00:00Z |
2026-10-30T00:00Z |
2026-10-30T00:00+09:00 |
2026-10-29T15:00Z |
2026-10-29T15:00Z |
2026-10-29T15:00Z |
2026-10-30 00:00 |
2026-10-29T15:00Z |
2026-10-30T04:00Z |
2026-10-30T00:00Z |
2026/10/30 |
2026-10-29T15:00Z |
2026-10-30T04:00Z |
2026-10-30T00:00Z |
2026-10 |
2026-10-01T00:00Z |
2026-10-01T00:00Z |
2026-10-01T00:00Z |
2026 |
2026-01-01T00:00Z |
2026-01-01T00:00Z |
2026-01-01T00:00Z |
2026-1-5 |
2026-01-04T15:00Z |
2026-01-05T05:00Z |
2026-01-05T00:00Z |
2026-10-30T24:00 |
2026-10-30T15:00Z |
2026-10-31T04:00Z |
2026-10-31T00:00Z |
(表では秒とミリ秒の :00.000 を省いた)
読み取れることは次のとおり。
- 日付だけの形(
2026-10-30/2026-10/2026)は、どのタイムゾーンでも同じ UTC の0時になった。2026-10は日が、2026は月と日が01で補われる。仕様の「欠けた MM・DD は01」の規定どおりである。 T付きでオフセットの無い形は、タイムゾーンごとにずれる。東京では UTC の前日15時、ニューヨーク(10月30日は夏時間で UTC-4)では UTC の4時になった。- オフセットを書けば(
Z/+09:00)、タイムゾーンに関係なく同じ瞬間になる。 - 形式外の
2026-10-30 00:00、2026/10/30、ゼロ埋めの無い2026-1-5は、V8 ではローカル時刻として読まれた。ただしこれは仕様ではなく実装の挙動なので、他のエンジンで同じになる保証はない。特に2026-1-5は「日付だけ」に見えるのに UTC ではなくローカルになり、ゼロ埋めの有無で結果が9時間変わる。 T24:00は翌日の0時になった。仕様の Note に、24:00と00:00で1日の終わりと始まりの真夜中を区別でき、1995-02-04T24:00と1995-02-05T00:00は同じ時点を指す、とある。
なぜ getDate() が1日ずれるのか
new Date('2026-10-30') は UTC の10月30日0時を指す。これを getDate() や toLocaleDateString() のようなローカル時刻のメソッドで読むと、その時点の現地時刻が返る。
- 東京(UTC+9)では10月30日9時なので、
getDate()は30。 - ニューヨーク(UTC-4)では10月29日20時なので、
getDate()は29。
UTC より東(日本など)では日付が変わらず、西(南北アメリカなど)では前日になる。日本だけで開発・テストしているとこのずれは表に出ない。
実際に試す
Node.js があれば試せる(記事では v24.11.1 で確認)。タイムゾーンは環境変数 TZ で切り替えられるが、Windows の Git Bash では TZ=America/New_York node ... が効かず東京のままだった(TZ=UTC は効いた)。どの OS でも同じに動かすため、以下のスクリプトはスクリプトの先頭で process.env.TZ を設定している。
1. 書き方ごとの解釈を比べる
1 | |
1 | |
'America/New_York' を 'Asia/Tokyo' や 'UTC' に変えると、上の表の他の列が再現できる。
2. ‘YYYY-MM-DD’ をローカル日付として読む
日付だけの文字列を「現地のその日」として扱いたいなら、文字列を new Date に渡さず、年・月・日の数値に分けて new Date(年, 月 - 1, 日) を使う。数値を渡す形はローカル時刻として解釈される。new Date(2026, 1, 30) が3月2日に繰り上がるように、存在しない日付は例外にならないので、組み立て後に年月日が一致するかを確かめて弾く。
1 | |
1 | |
(タイムゾーン名の表記は OS のロケールで変わる)
new Date(s) は10月29日20時になり、parseLocalDate は10月30日0時になった。逆に「UTC のその日」として揃えたいなら、'2026-10-30T00:00Z' のように Z を明示するか、Date.UTC(2026, 9, 30) を使い、読むときも getUTCDate() などの UTC 系メソッドを使う。どちらにするかを決めずに混ぜるのが、1日ずれの原因になる。
3. 日付だけの文字列リテラルを検出する
コード中の new Date('YYYY-MM-DD') と Date.parse('YYYY-MM-DD') を探すスクリプト。見つかると終了コード 1 を返すので、CI に入れられる。
1 | |
次の src/sample.js で試した。
1 | |
1 | |
(Windows で実行したのでパス区切りが \ になっている)
T00:00 付きの3行目は対象外にしている。リテラルではなく変数(new Date(input.value) など)に日付だけの文字列が入るケースはこの検出では拾えないので、入力元が <input type="date"> のように YYYY-MM-DD を返すものかどうかもあわせて確認したい。
ハシトシステムの UNIX 時間変換では
ブラウザで使える UNIX時間(タイムスタンプ)変換(ハシトシステム) の「日時 → タイムスタンプ」は、2026-10-30 時点のコードで <input type="datetime-local"> の値をそのまま new Date(value) に渡している。MDN によると datetime-local の値は YYYY-MM-DDTHH:mm の形で、タイムゾーンを持たない。T 付きでオフセットの無い「日付と時刻」の形なので、仕様どおりブラウザのローカル時刻として解釈される。日本のブラウザで 2026-10-30T00:00 を入れれば、日本時間の10月30日0時のタイムスタンプになる。
もしここが <input type="date">(値は YYYY-MM-DD)だったら、同じ new Date(value) でも UTC の0時になり、日本時間では9時のタイムスタンプが出ていたはずだ。入力欄の type を変えるときは、解釈の基準も変わる点に注意したい。
Date の挙動が絡む記事として、存在しない日付が繰り上がる話を含む4月1日生まれの記事 と、振替休日をJSで計算した記事 も書いた。
まとめ
- ECMAScript 仕様では、オフセットの無い
2026-10-30(日付だけ)は UTC、2026-10-30T00:00(日付と時刻)はローカル時刻として解釈される。 - そのため
new Date('2026-10-30').getDate()は、UTC より西のタイムゾーンでは前日を返す。日本では表に出にくい。 2026/10/30、2026-10-30 00:00、2026-1-5は仕様の形式外で、解釈は実装次第。Node.js v24.11.1 ではローカル時刻として読まれた。- ローカルの日付として扱うなら
new Date(年, 月 - 1, 日)、UTC として扱うならZを明示するかDate.UTCを使い、読む側のメソッドも揃える。
参考
- ECMAScript Language Specification: 21.4.1.32 Date Time String Format、21.4.3.2 Date.parse ( string )
- MDN Web Docs: Date.parse()
- MDN Web Docs:
<input type="datetime-local">