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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// parse.js: 同じ「2026年10月30日 0時」を書き方を変えて new Date に渡す
const inputs = [
'2026-10-30',
'2026-10-30T00:00',
'2026-10-30T00:00:00',
'2026-10-30T00:00Z',
'2026-10-30T00:00+09:00',
'2026-10-30 00:00',
'2026/10/30',
'2026-10',
'2026',
'2026-1-5',
'2026-10-30T24:00',
];
console.log('node', process.version, 'TZ', Intl.DateTimeFormat().resolvedOptions().timeZone);
for (const s of inputs) {
const d = new Date(s);
const iso = Number.isNaN(d.getTime()) ? 'Invalid Date' : d.toISOString();
console.log(JSON.stringify(s).padEnd(26), iso);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
$ node -e "process.env.TZ='America/New_York'; require('./parse.js')"
node v24.11.1 TZ America/New_York
"2026-10-30" 2026-10-30T00:00:00.000Z
"2026-10-30T00:00" 2026-10-30T04:00:00.000Z
"2026-10-30T00:00:00" 2026-10-30T04:00:00.000Z
"2026-10-30T00:00Z" 2026-10-30T00:00:00.000Z
"2026-10-30T00:00+09:00" 2026-10-29T15:00:00.000Z
"2026-10-30 00:00" 2026-10-30T04:00:00.000Z
"2026/10/30" 2026-10-30T04:00:00.000Z
"2026-10" 2026-10-01T00:00:00.000Z
"2026" 2026-01-01T00:00:00.000Z
"2026-1-5" 2026-01-05T05:00:00.000Z
"2026-10-30T24:00" 2026-10-31T04:00:00.000Z

'America/New_York' を 'Asia/Tokyo' や 'UTC' に変えると、上の表の他の列が再現できる。

2. ‘YYYY-MM-DD’ をローカル日付として読む

日付だけの文字列を「現地のその日」として扱いたいなら、文字列を new Date に渡さず、年・月・日の数値に分けて new Date(年, 月 - 1, 日) を使う。数値を渡す形はローカル時刻として解釈される。new Date(2026, 1, 30) が3月2日に繰り上がるように、存在しない日付は例外にならないので、組み立て後に年月日が一致するかを確かめて弾く。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// local-date.js: 'YYYY-MM-DD' をローカル日付の0時として読む
process.env.TZ = process.argv[2] || 'America/New_York';

function parseLocalDate(s) {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
if (!m) return null;
const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
const dt = new Date(y, mo - 1, d);
// 2026-02-30 のような存在しない日付は繰り上がるので弾く
if (dt.getFullYear() !== y || dt.getMonth() !== mo - 1 || dt.getDate() !== d) return null;
return dt;
}

const s = '2026-10-30';
const a = new Date(s);
const b = parseLocalDate(s);
console.log('TZ', process.env.TZ);
console.log('new Date(s).getDate() ', a.getDate(), a.toString());
console.log('parseLocalDate(s).getDate()', b.getDate(), b.toString());
console.log('parseLocalDate("2026-02-30")', parseLocalDate('2026-02-30'));
1
2
3
4
5
$ node local-date.js
TZ America/New_York
new Date(s).getDate() 29 Thu Oct 29 2026 20:00:00 GMT-0400 (アメリカ東部夏時間)
parseLocalDate(s).getDate() 30 Fri Oct 30 2026 00:00:00 GMT-0400 (アメリカ東部夏時間)
parseLocalDate("2026-02-30") null

(タイムゾーン名の表記は 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// find-date-only.js: new Date('YYYY-MM-DD') / Date.parse('YYYY-MM-DD') の直書きを探す
const fs = require('node:fs');
const path = require('node:path');

const re = /(new Date|Date\.parse)\(\s*(['"`])\d{4}-\d{2}-\d{2}\2\s*\)/g;
let found = 0;

function walk(dir) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
const p = path.join(dir, e.name);
if (e.isDirectory()) walk(p);
else if (/\.(c|m)?(j|t)sx?$/.test(e.name)) {
fs.readFileSync(p, 'utf8').split(/\r?\n/).forEach((line, i) => {
for (const m of line.matchAll(re)) {
found++;
console.log(`${p}:${i + 1}: ${m[0]} は UTC 0時として読まれます`);
}
});
}
}
}

walk(process.argv[2] || '.');
process.exitCode = found ? 1 : 0;

次の src/sample.js で試した。

1
2
3
const start = new Date('2026-10-30');
const end = Date.parse("2026-11-01");
const ok = new Date('2026-10-30T00:00');
1
2
3
$ node find-date-only.js src
src\sample.js:1: new Date('2026-10-30') は UTC 0時として読まれます
src\sample.js:2: Date.parse("2026-11-01") は UTC 0時として読まれます

(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">