入力された日付が正しいかをチェックするとき、「①形式が正しいか」と「②実在する日付か」は別物です。たとえば 2024-02-31 は形式は正しいですが、存在しない日付です。
注意したいのは、new Date() だけでは実在チェックにならない点です。2024-02-31 はエラーにならず 3月2日 に繰り越されてしまうため、「有効な日付」と誤判定されます。この記事では、この罠を避けた正しいチェック方法を解説します。
YYYY-MM-DD)を確認し、②new Date(年, 月-1, 日) で組み立てて年月日が入力と一致するか比較します。!isNaN(date.getTime()) だけでは、2024-02-31 のような繰り越される無効日付を見逃します。まず形式をチェックする(正規表現)
まずは入力が想定どおりの形式(ここでは YYYY-MM-DD)かを正規表現で確認します。これは「形式」だけのチェックで、日付が実在するかは見ていません。
function isDateFormat(str) {
return /^\d{4}-\d{2}-\d{2}$/.test(str);
}
isDateFormat("2024-07-13"); // true
isDateFormat("2024/07/13"); // false(区切りが違う)
isDateFormat("24-07-13"); // false(桁数が違う)
isDateFormat("2024-02-31"); // true ← 形式は正しい(実在は別問題)
最後の例のように、2024-02-31 も形式チェックは通過します。ここから「実在する日付か」を確かめる必要があります。
new Date() だけでは実在チェックにならない(繰り越しの罠)
「new Date() でパースして isNaN を見ればいい」と思いがちですが、これは不十分です。存在しない日付は、エラーにならず近い実在日へ繰り越されてしまうからです。
function isValidDateNG(str) {
return !isNaN(new Date(str).getTime());
}
// 実際の挙動(繰り越されてしまう)
new Date("2024-02-31"); // → 2024-03-02(true になる!)
new Date("2023-02-30"); // → 2023-03-02(true になる!)
new Date("2024-04-31"); // → 2024-05-01(true になる!)
new Date("2024-13-01"); // → Invalid Date(月の桁溢れだけは弾く)
2024-02-31 や 2023-02-30 は Invalid Date にならず、翌月へ繰り越されて「有効」と判定されます。そのため isNaN によるチェックだけでは、存在しない日付がすり抜けます。文字列パースとタイムゾーンの詳細は文字列をDate型に変換する方法でも解説しています。実在する日付かチェックする(年月日の一致比較)
確実なのは、入力の年月日からDateを組み立て、組み立てた結果が入力と一致するかを比べる方法です。繰り越しが起きると年月日がズレるので、それを検出できます。
function isValidDate(str) {
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(str);
if (!m) return false; // 形式が違えば不正
const y = Number(m[1]);
const mo = Number(m[2]);
const d = Number(m[3]);
// ローカルのDateで組み立てる(月は0始まりなので -1)
const date = new Date(y, mo - 1, d);
// 組み立て後の年月日が入力と一致すれば「実在する日付」
return (
date.getFullYear() === y &&
date.getMonth() === mo - 1 &&
date.getDate() === d
);
}
isValidDate("2024-07-13"); // true
isValidDate("2024-02-29"); // true(うるう年)
isValidDate("2023-02-29"); // false(平年に2/29は無い)
isValidDate("2024-02-31"); // false(繰り越しを検出)
isValidDate("2024-13-01"); // false
この方法なら、うるう年(2024-02-29)は通し、平年の 2023-02-29 や存在しない 2024-02-31 はきちんと false にできます(すべてNodeで動作確認済み)。
タイムゾーンのズレに注意
上のコードで new Date(y, mo - 1, d)(数値を渡すローカルのコンストラクタ)を使っているのには理由があります。文字列を new Date("2024-07-13") のように渡すと、ハイフン区切りはUTCとして解釈され、UTCより遅れた地域では getDate() が1日ズレることがあります。
年月日の一致比較をするときは、文字列パースに頼らず数値からローカルのDateを組み立てるのが安全です。なお 2024/07/13(スラッシュ区切り)はローカル時間として解釈されるなど、区切り文字でも挙動が変わります。詳細は文字列をDate型に変換する方法を参照してください。
ライブラリや新しいAPIを使う手も
厳密な日付検証を簡潔に書きたい場合は、ライブラリや新しいAPIも選択肢です。
- date-fns:
parseでフォーマット指定して読み込み、isValidで妥当性を確認できます。 - Temporal(新しい日付API):
Temporal.PlainDate.from(str, { overflow: "reject" })は不正な日付で例外を投げます(既定の"constrain"は丸めてしまうためrejectを指定)。
現在の日付を取得・表示する方法は現在の日付をYYYY/MM/DD形式で取得・表示する方法にまとめています。
よくある質問(FAQ)
/^\d{4}-\d{2}-\d{2}$/.test(str) で YYYY-MM-DD 形式を確認できます。ただしこれは形式のみのチェックで、2024-02-31 のような実在しない日付も通過します。実在チェックは別途必要です。new Date() は 2024-02-31 を 3/2 に繰り越すため、isNaN だけでは見逃します。new Date(年, 月-1, 日) で組み立てて、getFullYear() / getMonth() / getDate() が入力と一致するかを比較すると、確実に判定できます。2024-02-29(うるう年)は true、2023-02-29(平年)は 3/1 に繰り越されるため一致比較で false になります。"2024-07-13" はUTCとして解釈され、UTCより遅れた地域では getDate() が1日ズレることがあります。年月日の一致比較では、文字列パースではなく new Date(年, 月-1, 日) でローカルのDateを組み立てるのが安全です。まとめ
日付のチェックは、①正規表現で形式を確認 → ②年月日の一致比較で実在を確認、の2段階で行うのが確実です。
とくに注意すべきは、new Date() が 2024-02-31 を3/2 に繰り越して「有効」と誤判定する点です。isNaN だけに頼らず、new Date(年, 月-1, 日) で組み立てた結果が入力と一致するかを比べることで、存在しない日付を確実に弾けます。タイムゾーンのズレを避けるため、比較にはローカルのコンストラクタを使いましょう。
