日付を操作する必要があったので,いつものように Moment.js を使おうとした.JS ビルトインの Date
は操作を行うにはあまりにも使いづらいので,補助的なライブラリを使うのが定石になっている.8 より前の Java でいう Joda-Time みたいな存在.
リファレンスを見るために Moment.js の公式ページに行ったら,なにやら Luxon という新しいライブラリがあることに気づいた.これは Next Moment.js 的な新たに書き直されたライブラリらしい:
そういうことを調べているうちに他の日付操作のライブラリを見つけた.Day.js と date-fns だ.
どう違うのか
それぞれがどう違うのか.概要で比較するとこういう特徴があるように見えた:
ライブラリ | 特徴 |
---|---|
Moment.js | よく使われている.moment() という関数から日付のオブジェクトを作成して,そこからメソッドチェーンで操作ができることが特徴.jQuery Object みたいなインターフェイスになっている.タイムゾーンについてはアドオンが必要. |
Day.js | Moment.js と互換のインターフェイスを持っているが 2kB と軽量になっていることが特徴 |
Luxon | Moment.js と同じところで開発されているが,インターフェイスがまったく異なっている.よくあるオブジェクト指向の日付操作のライブラリのように DateTime , Interval , Duration などの役割ごとのクラスのインスタンスから操作を行うのが特徴.Immutable になっていることも特徴. |
date-fns | 関数型のインターフェイスが特徴.lodash のように時刻操作に対する種々の関数があり,通常の Date オブジェクトに対するヘルパーとして働くのが特徴 |
ISO 8601 形式の表現 からのインスタンスの作成で比較するとわかりやすいかもしれない:
ライブラリ | ISO 8601 形式からの作成 | 返ってくる型 |
---|---|---|
Moment.js | import moment from 'moment'; |
Moment |
Day.js | import dayjs from 'dayjs'; |
DayJs |
Luxon | import DateTime from 'luxon/src/datetime.js'; |
DateTime |
date-fns | import parseISO from 'date-fns/parseISO'; |
Date (builtin) |
インターフェイスの違いがよくわかる.
- Moment.js, Day.js はどちらも
jQuery
,$
関数のようなファクトリ関数に文字列を渡すことで暗黙的にパースされる. - Luxon は明示的に
DateTime.fromISO
というstatic
なファクトリメソッドを経由して作成する. - date-fns は
parseISO
というパースする関数をimport
して使い,ビルトインのDate
インスタンスを作成する.
というものになっている.
考察
(1) はなんでも受け取るファクトリ関数内で,与えられた引数に応じて暗黙的に判断されたオーバーロード関数があるかのようにふるまい,値をラップしたオブジェクトを返すインターフェイスだ.ちょうど jQuery の $(...)
や lodash の _(...)
のようなラッパーのライブラリにたまに見られるやつ.
(2) はごくごく標準的なオブジェクト指向のライブラリのインターフェイスだと思う.与える値の意味に応じて使用者側が明示的にクラスのファクトリメソッドを呼び出して生成するようになっている.返り値も DateTime
と明示的で具体的にものになっている.
(3) はヘルパー関数を適用するだけになっている.返ってくるオブジェクトもビルトインの Date
になっている.だからライブラリ特有のラッパークラスを使わないようになっている.
date-fns
のなにが面白いか
この中で date-fns が面白いと思った理由は,その関数型のアプローチにある.
たとえば他のライブラリ同様に,開始日時と終了日時を持つ Interval
というものがある.これはたとえば 2020-04-01 12:00
〜 2020-05-01 12:00
までという日時の範囲をあらわすのに使われ,いかにも欲しくなりそうな概念だ.
dete-fns はこの Interval
のインターフェイスのみ定義している.具体的なラッパークラスではなく,あくまで要求する型特性になっている:
type Interval = { start: Date | number end: Date | number }
date-fns/typings.d.ts at 21088144134ab581ea3a1f77485c645781534198 · date-fns/date-fns · GitHub
だから,この特性をとるオブジェクトであれば Interval
として与えることができるようになっている.ここが面白い点になっている.
// ok const int1 = { start: parseISO('2020-05-01T00:00:00'), // Date end: parseISO('2020-07-01T00:00:00'), // Date }; const dur1 = intervalToDuration(int1); console.log(dur1); // Object {days: 0, hours: 0, minutes: 0, months: 2, seconds: 0, years: 0}
https://runkit.com/mangano-ito/5ec2128c129f090013b3d455
同様に intervalToDuration
の返り値として Interval
から計算された期間をあらわす Duration
も型のみを定義している.
もちろん同じインターフェイスをもつクラスインスタンスでも可だ (Object
だし):
// this is also ok. const int2 = new class { get start() { return parseISO('2020-05-01T00:00:00'); } get end() { return parseISO('2020-07-01T00:00:00'); } }; const dur2 = intervalToDuration(int2); console.log(dur2);
https://runkit.com/mangano-ito/5ec21304a57d1b001ab1eeca
魔改造だけど Array.prototype
に getter
を生やしてもいける:
const int3 = [ parseISO('2020-05-01T00:00:00'), parseISO('2020-07-01T00:00:00'), ]; Object.defineProperty( Array.prototype, 'start', { get: function() { return this[0]; }, }, ); Object.defineProperty( Array.prototype, 'end', { get: function() { return this[1]; }, }, ); // this is ok, too! const dur3 = intervalToDuration(int3); console.log(dur3);
https://runkit.com/mangano-ito/5ec2132e25d80b001b43d325
他方,Luxon は Interval
等をライブラリのラッパークラスとしている.
const int1 = luxon.Interval.fromDateTimes( luxon.DateTime.fromISO('2020-05-01T00:00:00'), luxon.DateTime.fromISO('2020-07-01T00:00:00'), ); const dur1 = int1.toDuration(); console.log(dur1.toFormat("M 'months'")); // "2 months"
https://runkit.com/mangano-ito/5ec211dbb51f20001a795e52
Moment.js の Interval
と Duration
は独特だ:
const dur1 = moment('2020-05-01T00:00:00').to(moment('2020-07-01T00:00:00')); console.log(dur1); // "in 2 months"
https://runkit.com/mangano-ito/5ec212564de79e001ba6e774
Day.JS では RelativeTime
プラグインが必要だ.(ライブラリの軽量化のためかな?)
感想
有り体に言えば,オブジェクト指向のアプローチと関数型のアプローチの違い,というだけかもしれないが,date-fns は JavaScript の特性にうまく合った lodash のようなポリシーを持つ日時操作のライブラリとして面白いと思った.
そして,ドメイン内に外部ライブラリの概念を持ち込まないでよいところに良さを感じた.扱うデータは素朴な構造体でいいことになっている.ライブラリはあくまでインターフェイスに対するユースケースだけのシンプルな設計になっている.struct
, trait
, impl
みたいなかんじ.
しかしながらオブジェクト指向型のインターフェイスのほうが使いやすいというのはあるかもしれない.補完も効きやすいだろうし.外部のモデルを持ち込んでそのまま合成して使えるというのは楽だったりする.なので別に関数型至上主義というわけではない.
玉虫色の結論.いずれにせよ生の Date
を手で計算するのは辛いのでさけたい.