JavaScriptで日付を扱うと、素のDateメソッドは冗長な記述になり、整合せず、バグが発生しがちです。そこで日付操作に適したライブラリーが役立ちます。ライブラリーとJavaScriptのDateとの関係は、jQueryと素のDOM APIの関係のようなものです。
Stack Overflowで承認された回答から、月の最終日を取得する例で解説します。
var t = new Date();
alert( new Date(t.getFullYear(), t.getMonth() + 1, 0, 23, 59, 59) );
上記のコードは動くものの、getMonthに続く数字の意味を理解するのは困難です。次のコードを見てください。
const today = new Date();
console.log( lastDayOfMonth(today) );
lastDayOfMonthはdate-fnsのメソッドです。date-fnsは、ブラウザーとNode.jsでJavaScriptの日付を扱うための総合的なツールセットです。
この記事ではdate-fnsの設定と使い方を説明します。date-fnsを自分のプロジェクトに取り入れて、複数のヘルパーメソッドで簡単に日付を操作し、t.getMonth()+1,0,23,59,59のようなコードから解放されます。
Moment.jsではダメな理由
日付を操作するカテゴリーの代表はMoment.jsで、広く普及しています。Moment.js以外のJavaScriptデータライブラリーが必要なのでしょうか?
date-fnsを開発したSasha Kossによると、Moment.jsには特有の問題があり、date-fnsを作成したとのことです。GitHubのプロジェクトページから、要点を記載します。
- Moment.jsはmutableなのでバグを生む
- OOP APIが複雑(これがmutabeの問題を悪化させる)
- APIが複雑で、初回呼び出しのパフォーマンスが低い
- webpackと一緒に使うとlocaleファイルがincludeされるため、ビルドサイズが大きくなる
date-fnsと比較します(出典:プロジェクトのWebページ)。
- date-fnsはimmutableで、引数として渡した値を変更するのではなく、必ず新たなdateを返す
- APIがシンプルで、れぞれの関数が持つ機能は1つだけ
- 高速で、最高のユーザーエクスペリエンスを提供できる
- webpackと相性が良い。1関数1ファイルのスタイルなので、必要なものだけを選べばよく、不要な機能でプロジェクトが肥大化するのを防げる
Webベースのプロジェクトで使うには、CDNからdate-fnsを入手してページにドロップするだけです。date-fnsのメリットは使いやすさにあります。使い方を解説します。
インストール
ライブラリーをインストールする方法は複数あります。
dateFnsオブジェクトのnamespaceにライブラリーがインストールされます。jQueryがnamespace「$」を使うのと同じです。
<script>
console.log(
dateFns.isToday(new Date())
);
</script>
date-fnsの基本的な使い方
モジュールローダーを使っている場合は、使用する部分だけrequireできます。たとえば形式を指定して日付を出力します。
const format = require('date-fns/format');
console.log(
format(new Date(2017, 6, 6), 'MM/DD/YYYY')
);
// '06/07/2017'
localeを変更します。
const format = require('date-fns/format');
const deLocale = require('date-fns/locale/de') // German
console.log(
format(new Date(2017, 6, 6), 'DD MMMM YYYY', { locale: deLocale })
);
// 06 Juli 2017
サポートされている言語の一覧はこちらです(編注:日本語にも対応している)。
ブラウザーで作業をしている場合は、ヘルパークラスはdateFnsオブジェクトから呼び出します。月の最終日をlastDayOfMonthメソッドで調べましょう。
<script src="https://cdnjs.cloudflare.com/ajax/libs/date-fns/1.28.5/date_fns.min.js"></script>
<script>
console.log(
dateFns.lastDayOfMonth( new Date(2017, 6, 7) )
);
// Mon Jul 31 2017 00:00:00 GMT+0200 (CEST)
</script>
ここでは取り上げませんが、date-fnsには同様に週、四半期、年の最終日を調べるメソッドがあります。
Immutable、純粋関数、シンプル
date-fnsのメソッドは純粋関数で理解しやすいこともポイントです。読みやすいコードになり、デバッグで問題を特定しやすくなります。
Moment.jsのコードと比べて説明します。前述のとおりMomentのDateはmutableなので、予期しない挙動を示すことがあります。
const moment = require('moment');
const now = new Date();
const mNow = moment(now);
mNow.add('day', 3);
console.log(mNow.toDate());
mNow.add(3, 'day');
console.log(mNow.toDate());
// 2017-07-08T22:00:00.000Z
// 2017-07-11T22:00:00.000Z
ここで重要なことがいくつかあります。Momentのオブジェクトはmutableのため、add関数は引数を任意の順番で渡せます(1つ目のメソッドはdeprecation警告がでます)。ただし連続してaddを呼び出すと呼び出すたびに結果が変わり、混乱する恐れがあります。
mNow.add(3, 'day'); // add 3 days
mNow.add(3, 'day'); // adds 3 **more** days
date-fnsは引数の順番が規定されているため、関数は呼び出されるごとに新たなDateオブジェクトを作成し、必ず同じ結果を返します。
let addDays = required('date-fns/add_days');
addDays(now, 3); // (April 30, 2017)
addDays(now, 3); // (April 30, 2017)
addDays(now, 3); // (April 30, 2017)
メソッドには分かりやすい名前が付いているので(ただのaddではなくaddDays)、整合が取れていて、1つメソッドには1つの機能しか与えられていません。
日付の比較
たとえば公開日を日付で書いたり、何日前と書いたりするのを素のJavaScriptで実装するのは手間がかかりますが、date-fnsのdistanceInWordsメソッドを使えば簡単です。
2つの異なる日付を比較します。
const distanceInWords = require('date-fns/distanceInWords');
const startDate = new Date(2017, 3, 27); // (April 27, 2017)
const endDate = new Date(2018, 3, 27); // (April 27, 2018)
console.log(
distanceInWords(startDate, endDate)
);
// "about 1 year"
date-fnsでは次の単位で日付の差を出力できます。
- ミリ秒
- 秒
- 分
- 時間
- 日
- 月
- 年
使用するメソッド名はdifferenceIn[any of the above here]で、differenceInDaysのように使います。
日付のCollectionを扱う
date-fnsには日付のCollectionを柔軟に操作できるヘルパーメソッドがあります。
日付のCollectionを並び替える
compareAscで日付を昇順に並び替えましょう。compareAscは1つ目の日付が2つ目より後なら1を、前なら-1を、等しければ0を返します。
const compareAsc = require('date-fns/compare_asc');
const date1 = new Date('2005-01-01');
const date2 = new Date('2010-01-01');
const date3 = new Date('2015-01-01');
const arr = [date3, date1, date2];
const sortedDates = arr.sort(compareAsc);
// [ 2005-01-01, 2010-01-01, 2015-01-01 ]
日付が昇順に並びました。
compareAscと対になるメソッドはcompareDescです。
const compareDesc = require('date-fns/compare_desc');
...
const sortedDates = arr.sort(compareDesc);
// [ 2015-01-01, 2010-01-01, 2005-01-01 ]
2つの日付の間にある日を取得する
2つの日付の間にある日を取得するには、前述のaddDaysメソッドか指定の範囲に入る日付の配列を返すeachDay helperを使います。
const addDays = require('date-fns/add_days');
const eachDay = require('date-fns/each_day');
const today = new Date();
const dayAWeekFromNow = addDays(today, 7);
const thisWeek = eachDay(today, dayAWeekFromNow);
console.log(thisWeek);
// [ 2017-07-03, 2017-07-04, 2017-07-05, 2017-07-06, 2017-07-07, 2017-07-08, 2017-07-09, 2017-07-10 ]
もっとも近い日を調べる
日付の配列から指定する日付にもっとも近い日を取得するには、closestToメソッドを使います。次に示すコードは先ほどの例に続くものです。
const closestTo = require('date-fns/closest_to');
...
const christmas = new Date(2017, 11, 25);
const closestToChristmasDate = closestTo(christmas, thisWeek);
// 2017-07-10T
ほかにも配列のインデックスを取得するときに使うclosestIndexToメソッドもあります。
タイムゾーン
date-fnsのデメリットの1つは、Moment.jsのようにタイムゾーンを扱うヘルパー関数がないことです。Stack Overflowの回答を読むと、素のDateオブジェクトには「本当のタイムゾーン」のデータが入っていない背景を理解できます。date-fnsはコードを実行している現地の時間を返します。
先ほどのStack Overflowのスレッドでは素のJavaScriptでタイムゾーンを設定する方法が紹介されています。限定的ではありますが、出力時にUTCか現地時間から特定のタイムゾーンに変換できます。
new Date().toLocaleString("en-US", {timeZone: "America/New_York"});
MomentJSにはタイムゾーンを扱う別のライブラリーがあります。date-fnsがタイムゾーンをサポートするという計画が進行中ですが、執筆時点では未完成です。
最後に
date-fnsは、JavaScriptで日付を扱うヘルパークラスがそろった小規模なライブラリーです。この記事がdate-fnsのことを知って使いたくなる一助になれば幸いです。date-fnsは現在も活発に開発が続いており、バージョン2がまもなくリリースされる予定です。
(原文:Introduction to date-fns – a Lightweight JavaScript Date Library)
[翻訳:内藤 夏樹/編集:Livit]