JS の日時操作ライブラリを比較する: date-fns のインターフェイスがイカす

日付を操作する必要があったので,いつものように Moment.js を使おうとした.JS ビルトインの Date は操作を行うにはあまりにも使いづらいので,補助的なライブラリを使うのが定石になっている.8 より前の Java でいう Joda-Time みたいな存在.

リファレンスを見るために Moment.js の公式ページに行ったら,なにやら Luxon という新しいライブラリがあることに気づいた.これは Next Moment.js 的な新たに書き直されたライブラリらしい:

moment.github.io

そういうことを調べているうちに他の日付操作のライブラリを見つけた.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('2020-04-01T00:00:00');
Moment
Day.js
import dayjs from 'dayjs';
dayjs('2020-04-01T00:00:00');
DayJs
Luxon
import DateTime from 'luxon/src/datetime.js';
DateTime.fromISO("2020-04-01T00:00:00");
DateTime
date-fns
import parseISO from 'date-fns/parseISO';
parseISO('2020-04-01T00:00:00');
Date (builtin)

インターフェイスの違いがよくわかる.

  1. Moment.js, Day.js はどちらも jQuery, $ 関数のようなファクトリ関数に文字列を渡すことで暗黙的にパースされる.
  2. Luxon は明示的に DateTime.fromISO という static なファクトリメソッドを経由して作成する.
  3. 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 のインターフェイスのみ定義している.具体的なラッパークラスではなく,あくまで要求する型特性になっている:

date-fns.org

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 を手で計算するのは辛いのでさけたい.