Moment.jsは非推奨だが安易にdate-fnsに変えるな。date-fnsのisValidではparseを活用せよ
問題点
面倒な日付処理をやってくれるMoment.jsですが、mutable設計が混乱を招くとして、今後は非推奨と発表しています。immutableに変更する作業も実施しないとのこと。
https://momentjs.com/guides/#/lib-concepts/mutability/
The moment object in Moment.js is mutable. This means that operations like add, subtract, or set change the original moment object.
var a = moment('2016-01-01');
var b = a.add(1, 'week');
a.format();
"2016-01-08T00:00:00-06:00" // aも変更されてしまう。a.clone().add(1, 'week');とする必要あり
代替案として date-fns
にする人が多いのですが、
結論から言うと 「安易にdate-fnsに変えるな」 です。そちらにも罠があるから。
Moment.jsにもdate-fnsにも isValid
がありますが、仕様が異なるので注意が必要です。
実在しない日付、例えば2022年2月29日を引数に与えるとどうなるでしょう?
Moment.jsのisValidでは false
になりますが、date-fnsのisValidは true
になります。
- Moment.js
moment('2022-13-01','YYYY-MM-DD',true).isValid() //false
moment('2022-01-32','YYYY-MM-DD',true).isValid() //false
moment('2022-02-29','YYYY-MM-DD',true).isValid() //false
moment('2022-04-31','YYYY-MM-DD',true).isValid() //false
- date-fns
各月31日までなら、実在しない日付でも true
を返してしまう。
2月や閏年、30日までの月の扱いには注意が必要。
dateFns.isValid(new Date('2022-13-01')) //false
dateFns.isValid(new Date('2022-01-32')) //false
dateFns.isValid(new Date('2022-02-29')) //true
dateFns.isValid(new Date('2022-04-31')) //true
対策1
32日や13月は false
を返すので、Moment.jsの isValid と同じ使い方をしたいのであれば、date-fns の次の特徴を利用します。
- 2022-02-29(閏年以外の2/29)は、2022-03-01として処理されてしまう。
- 31日までは無条件にtrue。
- 実在しない日付は自動で差分が加算される。
だから、次のようにします。
※コメントをいただきまして、修正しました
-
date-fns
のバージョンは1.28.5
です。
const isValidDateString = (str, format) => {
const formatString = dateFns.format(
dateFns.parse(str, format),
format
)
return formatString === str;
}
isValidDateString('2022-02-27', 'YYYY-MM-DD') // true
isValidDateString('2022-02-28', 'YYYY-MM-DD') // true
isValidDateString('2022-02-29', 'YYYY-MM-DD') // false
isValidDateString('2022-02-30', 'YYYY-MM-DD') // false
isValidDateString('2022-02-31', 'YYYY-MM-DD') // false
isValidDateString('2022-02-32', 'YYYY-MM-DD') // false
isValidDateString('2022-02-33', 'YYYY-MM-DD') // false
- CodePenで試した結果
-
date-fns
のバージョンは1.28.5
です。
-
これでdate-fnsを使用しながら、Moment.jsのisValidと同じ結果を得られます。
対策2
またはこちら。
※コメントをいただきました
- CodePenで試した結果
-
date-fns
のバージョンは2.28.0
です。
-
これでdate-fnsを使用しながら、Moment.jsのisValidと同じ結果を得られます。
その他の候補
date-fns以外の候補はこちらです。
全て試してください。今のプロジェクトに合うものを選ぶと良いと思います。
Discussion
こちら標準のDate の問題みたいです。
date-fns だと次のような内容を出力していました。
次のような関数を作って、文字列をパースして得た日付型から、再度フォーマットかけて
一致を確認するといいかも。
いろいろ気をつけないといけないことが多いですね。
コメント失礼します。
こちらで date-fns による意図に反する挙動として挙げられているものは、 JS の Date オブジェクトの挙動に起因するものです。
Date は日付を表す文字列を受け取った場合にそれをパースして Date オブジェクトを作りますが、このときに存在しない日付が渡されてもある程度は柔軟に対応します。
今回で言えば、
という挙動がそれにあたります。
31日までは実在する日付に置き換えられてしまうので、日付文字列のパースにDateを利用している記事内のコード↓
では、例の問題が発生しています。
この問題は、日付のパースに Date ではなく
dateFns.parse()
を用いることで解決できます。実在しない日付である
2022-02-29
の isValid で false を得られています。記事内のコードも同様です↓
記事で紹介されている問題はDateによるものなので、記事タイトルの
という記述は誤りだと思います。
date-fns は何も悪くないのでコメントしてしまいました、偉そうにすみません🙇
2022/03/31 追記
date-fns によって正しく日付文字列のパースができる例を置いておきます。
記事内のCodePenはdate-fnsのバージョンが非常に古いものになっていましたので、こちらで確認することをおすすめします。
タイトルを変更しました!
おかしいな...
私の指摘したところと、lemonadernさんの指摘したところで動きが違う。
hooyanさんのCodePenの所を改良して動かしてみました。
結果:Tue Mar 01 2022 00:00:00 GMT+0900 (日本標準時) true true
dateFnsのバージョン違いがあるのか、何かパース時のオプション設定があるのかな。
記事で挙げられている CodePen を覗いてみたんですが、CDNとして読み込まれているdate-fnsのバージョンが古いものになっていました。(
v1.28.5
)2021/03/31 現在の最新バージョンは
v2.28.0
なので、かなり古いことがうかがえます。またdocsによれば、
parse
は v2 から挙動が大幅に変化しています。v2.28.0
の date-fns を利用したサンプルを作ってみたのでぜひ覗いてみてください〜2022-02-29 などの実在しない日付はアウトにした方がいいですよね!
調べていただき、疑問がはれました。
お手間かけてしまいましたが、ありがとうございます。
isValidは未実装ですが、こちらもあります。