moment.js で i18n 対応したり期間を文字列化する時の注意点

今らなら immutable で軽くて i18n にも対応してるってことで date-fns 使うのが良いのだろうけど、moment.js の i18n 対応でいろいろあったので、一応メモっておく。

ペルシア語の format() メソッドに注意

例えば、API向けに投げる日付データとして、YYYY-MM-DD な値を用意する場合、日本語や英語環境なら普通に format() メソッドを使えば問題はない。

// 日本語
moment.locale('ja');
console.log('lang', lang);
console.log(moment().format('YYYY-MM-DD'));
// -> 2018-02-05

// 英語
moment.locale('ja');
console.log('lang', lang);
console.log(moment().format('YYYY-MM-DD'));
// -> 2018-02-05

ところが、同じことをペルシア語でやるとペルシア数字に変換されちゃう。

// ペルシア語
moment.locale('fa');
console.log('lang', lang);
console.log(moment().format('YYYY-MM-DD'));
// -> ۲۰۱۸-۰۲-۰۵

画面に表示する場合はこれでよいけど、APIに投げるデータとしては問題がある。なので moment.localeData().preparse() を使って YYYY-MM-DD に変換する。

// ペルシア語
moment.locale('fa');
console.log('lang', lang);
console.log(moment.localeData().preparse(moment().format('YYYY-MM-DD')));
// -> 2018-02-05
// 日本語
moment.locale('ja');
console.log('lang', lang);
console.log(moment.localeData().preparse(moment().format('YYYY-MM-DD')));
// -> 2018-02-05

各言語毎の言葉で月を求めるには months() メソッド

これは知ってると便利そうなのでメモ。

// 日本語
moment.locale('ja');
moment.months();
// -> ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"]

// 英語
moment.locale('en');
moment.months();
// -> ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]

// ペルシア語
moment.locale('fa');
moment.months();
// -> ["ژانویه", "فوریه", "مارس", "آوریل", "مه", "ژوئن", "ژوئیه", "اوت", "سپتامبر", "اکتبر", "نوامبر", "دسامبر]

fromNow() 「○ヶ月前」を求める時のデフォルト閾値に注意

fromNow() メソッド使うと「○ヶ月前」的な文字列が求められる。

console.log(moment().subtract(25, 'day').fromNow());
// -> 25日前

だけど26日前にすると...

console.log(moment().subtract(26, 'day').fromNow());
// -> 1ヶ月前

なぜか26日前が1ヶ月前扱いになってしまう。

relativeTimeThreshold とかいう閾値を設定したり、求めたりするメソッドがある。

duration.humanize has thresholds which define when a unit is considered a minute, an hour and so on. For example, by default more than 45 seconds is considered a minute, more than 22 hours is considered a day and so on. To change those cutoffs use moment.relativeTimeThreshold(unit, limit) where unit is one of ss, s, m, h, d, M.

デフォルトでは45秒以上が1分とみなされ、22時間以上は1日とみなされるらしい。

他のデフォルトも見てみると....

console.log(moment.relativeTimeThreshold('ss')); // 44
console.log(moment.relativeTimeThreshold('s'));  // 45
console.log(moment.relativeTimeThreshold('m'));  // 45
console.log(moment.relativeTimeThreshold('h'));  // 22
console.log(moment.relativeTimeThreshold('d'));  // 26
console.log(moment.relativeTimeThreshold('M'));  // 11

うーん、こんなんなってるのか...

閾値を調整して再度試してみる。

moment.relativeTimeThreshold('ss', 60);
moment.relativeTimeThreshold('s', 60);
moment.relativeTimeThreshold('m', 60);
moment.relativeTimeThreshold('h', 24);
moment.relativeTimeThreshold('d', 30);
moment.relativeTimeThreshold('M', 12);

console.log(moment().subtract(25, 'day').fromNow());
// -> 25日前
console.log(moment().subtract(26, 'day').fromNow());
// -> 26日前

ちゃんと26日になった。

念のため、3ヶ月先ぐらいまでチェックしてみる、1ヶ月づつ遡って前後5日間をチェック。

moment.relativeTimeThreshold('ss', 60);
moment.relativeTimeThreshold('s', 60);
moment.relativeTimeThreshold('m', 60);
moment.relativeTimeThreshold('h', 24);
moment.relativeTimeThreshold('d', 30);
moment.relativeTimeThreshold('M', 12);

console.log('now', moment().format('YYYY/MM/DD'));
Array(3).fill(null).forEach((_, i) => {
  console.log('----------');

  // 1ヶ月先の -5 〜 +5 日間をチェック
  const base = moment().subtract(i + 1, 'months').add(5, 'day');
  Array(10).fill(null).forEach(() => {
    const d = base.subtract(1, 'day');
    console.log(d.format('YYYY/MM/DD'), d.fromNow());   
  });
});

ん、なんか変。

now 2018/02/06
----------
2018/01/10 27日前
2018/01/09 28日前
2018/01/08 29日前
2018/01/07 1ヶ月前
2018/01/06 1ヶ月前
2018/01/05 1ヶ月前
2018/01/04 1ヶ月前
2018/01/03 1ヶ月前
2018/01/02 1ヶ月前
2018/01/01 1ヶ月前
----------
2017/12/10 2ヶ月前
2017/12/09 2ヶ月前
2017/12/08 2ヶ月前
2017/12/07 2ヶ月前
2017/12/06 2ヶ月前 <-- この辺から2ヶ月前でないの?
2017/12/05 2ヶ月前
2017/12/04 2ヶ月前
2017/12/03 2ヶ月前
2017/12/02 2ヶ月前
2017/12/01 2ヶ月前
----------
2017/11/10 3ヶ月前
2017/11/09 3ヶ月前
2017/11/08 3ヶ月前
2017/11/07 3ヶ月前
2017/11/06 3ヶ月前 <-- この辺から3ヶ月前でないの?
2017/11/05 3ヶ月前
2017/11/04 3ヶ月前
2017/11/03 3ヶ月前
2017/11/02 3ヶ月前
2017/11/01 3ヶ月前

どうも、relativeTimeRounding() って言うメソッドで指定できるコールバック処理のデフォルト処理が、formNow()メソッドで返す日付を四捨五入しちゃってるらしい(1.6ヶ月的な数値を2ヶ月にしちゃってる)。

と、SallyAcolyte さんが教えてくれた(ありがとうございます!)

なので、以下の様に Math.floor使って、端数を切り捨てるようにする。

moment.relativeTimeRounding(Math.floor);

あらためて実行!

now 2018/02/06
----------
2018/01/10 27日前
2018/01/09 28日前
2018/01/08 29日前
2018/01/07 1ヶ月前
2018/01/06 1ヶ月前
2018/01/05 1ヶ月前
2018/01/04 1ヶ月前
2018/01/03 1ヶ月前
2018/01/02 1ヶ月前
2018/01/01 1ヶ月前
----------
2017/12/10 1ヶ月前
2017/12/09 1ヶ月前
2017/12/08 1ヶ月前
2017/12/07 1ヶ月前
2017/12/06 2ヶ月前 <-- ここから2ヶ月前
2017/12/05 2ヶ月前
2017/12/04 2ヶ月前
2017/12/03 2ヶ月前
2017/12/02 2ヶ月前
2017/12/01 2ヶ月前
----------
2017/11/10 2ヶ月前
2017/11/09 2ヶ月前
2017/11/08 2ヶ月前
2017/11/07 2ヶ月前
2017/11/06 3ヶ月前 <-- ここから3ヶ月前
2017/11/05 3ヶ月前
2017/11/04 3ヶ月前
2017/11/03 3ヶ月前
2017/11/02 3ヶ月前
2017/11/01 3ヶ月前

良さそう!