かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript day.js, date-fns で実在する日付かどうか判定したい

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() で日付の妥当性を判定できる

// moment.js
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());
});
/*
2021-04-30, true
2021-04-31, false
2021-04-32, false
2021-02-30, false
*/

day.js, date-fns の isValid メソッドは日付の妥当性は判定しない

day.js

dayjs().isValid()

// day.js
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());
});
/*
2021-04-30, true
2021-04-31, true
2021-04-32, false
2021-02-30, true
*/

cf. Validation · Day.js

date-fns

isValid(date)

// date-fns
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)));
});
/*
2021-04-30, true
2021-04-31, true
2021-04-32, false
2021-02-30, true
*/

cf. date-fns - modern JavaScript date utility library

2021-04-32 は共に false になっていますが、2021-04-312021-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));
});
/*
2021-04-30, Fri Apr 30 2021 09:00:00 GMT+0900
2021-04-31, Sat May 01 2021 09:00:00 GMT+0900
2021-04-32, Invalid Date
2021-02-30, Tue Mar 02 2021 09:00:00 GMT+0900
*/

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'));
});
/*
2021-04-30T00:00+0900, true
2021-04-31T00:00+0900, false
2021-04-32T00:00+0900, false
2021-02-30T00:00+0900, false
*/

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"));
});
/*
2021-04-30T00:00+0900, true
2021-04-31T00:00+0900, false
2021-04-32T00:00+0900, false
2021-02-30T00:00+0900, false
*/

cf. date-fns - modern JavaScript date utility library

こちらも日付の妥当性が判定できました!

動作サンプル

所感

day.js, date-fns には isValid 関数が合ったので日付の妥当性が判定できるものだと思っていたのですが、予期しない挙動でびっくりしました。共に大元の Date の挙動だと分かったのですが、どうしてこんな挙動に…と思うばかりです。
他にも JavaScriptDate には不思議な挙動が多いので JavaScript での日付の操作はかなり注意するか極力行わないようにするのが良いのではないかと思いました。

Date の不思議な挙動の例

YYYY-MM-DD を渡すと UTC 00:00:00 時になるが、それ以外は locale での 00:00:00 時になる

new Date('2021-12-10');
// Fri Dec 10 2021 09:00:00 GMT+0900
new Date('2021/12/10');
// Fri Dec 10 2021 00:00:00 GMT+0900
new Date(2021, 11, 10);
// Fri Dec 10 2021 00:00:00 GMT+0900

[参考]

カレンダーの操作難しいね!