目的
Webフォーム上で西暦日付入力欄を作成する際、inputタグのtype属性でdateがサポートされていないブラウザではカレンダー形式の入力ができず不便なので、jQuery UIのdatepickerなどに頼ることがあるはず。ただ、その場合だと「2019/2/29」などの無効な日付を入力することができてしまうため、(サーバサイドは当然ながら)クライアントサイドでバリデーションすることが必要になる。
そこで、JavaScriptで日付文字列のバリデーションする関数を示す。併せて、JavaScriptのDateオブジェクトの罠を1つだけ紹介する。
日付バリデーション関数
function isValidDate(text) {
if (!/^\d{1,4}(\/|-)\d{1,2}\1\d{1,2}$/.test(text)) {
return false;
}
const [year, month, day] = text.split(/\/|-/).map(v => parseInt(v, 10));
return year >= 1
&& (1 <= month && month <= 12)
&& (1 <= day && day <= daysInMonth(year, month));
function daysInMonth(year, month) {
if (month === 2 && isLeapYear(year)) {
return 29;
}
return {
1: 31, 2: 28, 3: 31, 4: 30,
5: 31, 6: 30, 7: 31, 8: 31,
9: 30, 10: 31, 11: 30, 12: 31
}[month];
}
function isLeapYear(year) {
return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0);
}
}
はじめに<1-4ケタの数字><セパレーター(/ or -)><1-2ケタの数字><最初のセパレーターと同じ文字><1-2ケタの数字>のパターンになっているかを正規表現で確認する。これによって、入力パターンが無限個から有限個に絞られ、後続の処理で考慮しなければならないことがだいぶ減る。
あとは年、月、日の値がそれぞれ範囲内に収まっているかをうるう年を考慮しながら調べるだけ。
改良版?
https://kantaro-cgi.com/blog/javascript/javascript_date_check.html (2018/4/13 アクセス)
たまたま見つけたリンク先を参考にして改良?したコードがこれ。
function isValidDate(text) {
if (!/^\d{1,4}(\/|-)\d{1,2}\1\d{1,2}$/.test(text)) {
return false;
}
const [year, month, day] = text.split(/\/|-/).map(v => parseInt(v, 10));
// ここまでは一緒で、ここから下が変更点
const date = new Date(year, month - 1, day);
return date.getFullYear() === year
&& date.getMonth() === month - 1
&& date.getDate() === day;
}
リンク先のコードは、コンストラクタ関数じゃないのに関数名の先頭が大文字になっていたり、let(あるいはvar)もなしに関数内で変数を宣言・初期化していたりとツッコミどころはあるが、はじめに自分が書いたコードと比べると短くて、よりエレガントだなとそこは素直に降参。
C#ではDateTime構造体のコンストラクタに無効な年月日(ex. 2019/2/29)の数値を与えると例外がスローされるので、これを利用するとバリデーション処理がシンプルになるが、JavaScriptのDateオブジェクトのコンストラクタでは例外はスローされなかった(2019年3月1日のインスタンスが生成される)ので、そこで諦めていたがこんなに上手い方法があるとはと感心した…が、
Dateコンストラクタ関数の罠
ところが改良後のコードをテストすると有効な日付(ex. 0001/01/01)なのにfalseが返ってくることがある。
なので、ちょっとMDNのDateオブジェクトのページを調査。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date (2018/4/13 アクセス)
コンストラクタ関数の引数 year の説明によると、
year
年を表す整数値です。0 から 99 までの値は、1900 から 1999 の値にマッピングされます。互換性のために (2000 年問題を回避するために)、常に完全な年を指定するべきです。例えば 98 より も 1998 を使いましょう。後述の例 を参照。
とのこと。
よって、改良したつもりのコードにはバグがあり、利用してはいけないことが分かった。
裏話
昔、オフショアしていて、国外にいるデベロッパーさんに「日付のバリデーション処理忘れずにお願いします」と頼んだが、ピュア?な正規表現だけ(手続き型の処理が一切含まれてないの意)で実装されたものがあがってきた。
試しに正規表現部分をコピーしてエディタに貼り付けたところ文字数が500文字を超えているのを見て目眩がしたが、テストしてみると直近のうるう年などの対応はうまくできているっぽい。
が、あまりにも大きな正規表現だったため、すべてのケースをテストしてみないとレビューOKとする自信がなく、早々にNGにしてやり直しを要求した、というか自分でこの記事で紹介したコードを書いて送った。
正規表現だけでうるう年とかも考慮した日付のバリデーションって実現できるのだろうか?