Moment.js くんが卒業してからフロントエンドでの日付操作には day.js か date-fns を使っていました。
今回年・月・日が別々の select ボックスで日付を選択する UI があり 2月31日のような存在しない日付が選べてしまうので、存在しない日付を判定しようとして思ったより根が深かったのでメモ。
day.js, date-fns には存在しない日付を判定するメソッドがなかった
Moment.js くんの moment('DATE'). isValid()
は存在しない日付をチェックできたのですが、day.js, date-fns の日付チェックのメソッドは完全に存在しない日付を判定できるわけではないようです。
moment.js は isValid()
で日付の妥当性を判定できる
import moment from "moment";
const dateList = [
'2021-04-30',
'2021-04-31',
'2021-04-32',
'2021-02-30',
];
dateList.forEach((date) => {
console.log(date, moment(date).isValid());
});
day.js, date-fns の isValid メソッドは日付の妥当性は判定しない
day.js
dayjs().isValid()
import dayjs from "dayjs";
const dateList = [
'2021-04-30',
'2021-04-31',
'2021-04-32',
'2021-02-30',
];
dateList.forEach((date) => {
console.log(date, dayjs(date).isValid());
});
cf. Validation · Day.js
date-fns
isValid(date)
import { isValid } from "date-fns";
const dateList = [
'2021-04-30',
'2021-04-31',
'2021-04-32',
'2021-02-30',
];
dateList.forEach((date) => {
console.log(date, isValid(new Date(date)));
});
cf. date-fns - modern JavaScript date utility library
2021-04-32
は共に false
になっていますが、2021-04-31
や 2021-02-30
といった実在しない日付が true
になっています。なんでや!
day.js, date-fns は内部的に Date
が使われているので Date
の挙動が影響している
const dateList = [
'2021-04-30',
'2021-04-31',
'2021-04-32',
'2021-02-30',
];
dateList.forEach((date) => {
console.log(date, new Date(date));
});
Date は 32日というどの月にも存在しない日を指定している日付は Invalid Date
になりますが、4/31 や 2/30 といった日付は過ぎている日分翌月にずれる仕様となっているようです。
なので day.js, date-fns での isValid
メソッドは Invalid Date
になる日付以外は Date で日付になるので true
になってしまうものと思われます。
しかし '2021-02-31' のような日付は存在しないので true
になってしまうとちょっと困ります…
元の日付と文字列比較をすれば日付の妥当性を判定できる
実在しない日付は Date を通すと日付が変わってしまうので、これを逆手に取って元の日付と同じフォーマットで文字列比較をすれば実在しない日付の場合は一致しないので日付の妥当性を判別できそうです。
day.js, date-fns それぞれで日付を文字列にして比較判定する関数を作成します。(day.js, date-fns でフォーマットの指定方法が微妙に異なるのがちょい面倒です…)
day.js
isValidDate.ts
import dayjs from "dayjs";
export const isValidDate = (
dateStr: string,
format: string = "YYYY-MM-DD"
): boolean => {
const formatDate = dayjs(dateStr, format).format(format);
return dateStr === formatDate
};
app.ts
import { isValidDate } from './isValidDate'
const dateList = [
'2021-04-30T00:00+0900',
'2021-04-31T00:00+0900',
'2021-04-32T00:00+0900',
'2021-02-30T00:00+0900',
];
dateList.forEach((date) => {
console.log(date, isValidDate(date, 'YYYY-MM-DDTHH:mmZZ'));
});
cf. String + Format · Day.js
意図したとおりに日付の妥当性が判定できました!
date-fns
date-fns も同じように判定する関数を作成できるが、Invalid Date
を format しようとするとエラーになるので注意が必要です
isValidDate.ts
import { format as DateFormat } from "date-fns";
export const isValidDate = (
dateStr: string,
format: string = "yyyy-MM-dd"
): boolean => {
const d = new Date(dateStr);
try {
const formatDate = DateFormat(d, format);
return dateStr === formatDate;
} catch (error) {
return false;
}
};
app.ts
import { isValidDate } from './isValidDate'
const dateList = [
'2021-04-30T00:00+0900',
'2021-04-31T00:00+0900',
'2021-04-32T00:00+0900',
'2021-02-30T00:00+0900',
];
dateList.forEach((date) => {
console.log(date, isValidDate(date, "yyyy-MM-dd'T'HH:mmxxxx"));
});
cf. date-fns - modern JavaScript date utility library
こちらも日付の妥当性が判定できました!
動作サンプル
所感
day.js, date-fns には isValid
関数が合ったので日付の妥当性が判定できるものだと思っていたのですが、予期しない挙動でびっくりしました。共に大元の Date
の挙動だと分かったのですが、どうしてこんな挙動に…と思うばかりです。
他にも JavaScript の Date
には不思議な挙動が多いので JavaScript での日付の操作はかなり注意するか極力行わないようにするのが良いのではないかと思いました。
Date の不思議な挙動の例
YYYY-MM-DD
を渡すと UTC 00:00:00 時になるが、それ以外は locale での 00:00:00 時になる
new Date('2021-12-10');
new Date('2021/12/10');
new Date(2021, 11, 10);
[参考]
カレンダーの操作難しいね!