何故Dateオブジェクトではなく日付操作ライブラリなのか？

JavaScriptでの製品開発において、日付を扱う場合は大抵moment.jsやdate-fnsなどのライブラリが活用される。
今までなんとなくライブラリを使ってきたが、js組み込みのDateオブジェクトだってそれなりに機能は充実しているように見える。少なくともメソッドは一杯ある。
Dateオブジェクトの何が辛いのか調べてみた。

# よくあるやりたいこと
jsで日付を扱う時には、大抵こんなことがやりたい。

- YYYY/MM/DD など日本人にとって一般的なフォーマットで表示したい 英語版なら英語用のフォーマットも用意したい
- サーバーのシステム日付に依存せず、常に特定のタイムゾーンを使用したい
- 取得した日付を別のフォーマットに直して扱いたい

この辺りについてDateオブジェクトを使う場合とライブラリを使う場合でどんな差が出るかを比較してみる
今回は知名度もシェアも高いmoment.jsを比較対象のライブラリとする。

## 日付をフォーマットする
現在時刻を「2020/5/12 21時2分44秒」のようなフォーマットフォーマットに変換する

### Dateオブジェクトの場合
```javascript
const now = new Date();

const year = now.getFullYear().toString();
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const date = now.getDate().toString().padStart(2, '0');
const hour = now.getHours().toString().padStart(2, '0');
const minutes = now.getMinutes().toString().padStart(2, '0');
const seconds = now.getSeconds().toString().padStart(2, '0');

const formatted = `${year}/${month}/${date} ${hour}時${minutes}分${seconds}秒`; 

console.log(formatted);
// 2020/05/12 21時02分44秒 のような形式で現在時刻が出力される
```
見るからに冗長。
年月日...とそれぞれの要素を個別に取得して文字列変換する必要がある。
わかりづらいのは **getMonth()の返り値が0から始まる月の番号** なので+1しないと使えない。
加えて年の取得はgetYear()ではなくgetFullYear()で取得する必要がある。

### moment.jsの場合
```javascript
const moment = require('moment');

const FORMAT = 'YYYY/MM/DD HH時mm分ss秒';
const formatted = moment().format(FORMAT);

console.log(formatted);
// 2020/05/12 21時02分44秒 のような形式で現在時刻が出力される
```
非常に楽。フォーマットを文字列として定義できるので可読性も高い。
多言語対応の場合は言語ごとにフォーマットを定数で用意して、引数を切り替えるだけでいい。


## タイムゾーンを指定する
どんな環境で動こうとも必ず日本時間を扱いたい！　というケース

クラウドプラットフォームではサーバーのシステム時刻が外国時間であることが多々あり、タイムゾーンが指定されていないとアプリケーションの動作に影響を及ぼす。
日本サーバー上で動作するアプリケーションだったとしても、Circle CIでテストを動かすとタイムゾーンの違いでテストが通らなかったりして開発に影響を及ぼすこともある。

### Dateオブジェクトの場合
```javascript
// 「日付をフォーマットする」の部分を関数化した
const formatDate = require('./formatDate');

const utc = new Date().toLocaleString('en-US', { timeZone: 'UTC' });
const formattedUTC = formatDate(new Date(utc));

const jst = new Date().toLocaleString('ja', { timeZone: 'Asia/Tokyo' });
const formattedJST  = formatDate(new Date(jst));

console.log("UTC", formattedUTC);
// UTC 2020/05/10 09時56分27秒

console.log("JST", formattedJST);
// JST 2020/05/10 18時56分27秒
```
一度LocaleStringを作成して、その文字列からさらにDateオブジェクトを作成する流れになる。
耐えられなくは無いけど冗長に感じる。
あと、関数化して誤魔化しているが当然フォーマットは前の項目で書いた長いコードが必要になる。


### moment.jsの場合
```javascript
const moment = require('moment-timezone');

const FORMAT = 'YYYY/MM/DD HH時mm分ss秒';

const formattedUTC = moment().tz('UTC').format(FORMAT);
const formattedJST = moment().tz('Asia/Tokyo').format(FORMAT);

console.log('UTC', formattedUTC);
// UTC 2020/05/10 10時23分45秒
console.log('JST', formattedJST);
// JST 2020/05/10 19時23分45秒
```
タイムゾーンを扱う場合は拡張版のmoment-timezoneを使用する。
タイムゾーンの指定からフォーマット適用までメソッドチェーンで１行で書けるので非常にスッキリしていて読みやすいし書きやすい。

## 取得した日時（ISO準拠）を別のフォーマットに変換したい
広く一般的に用いられる 2020-05-26T14:11:06 ISO 8601形式のパースを考える。

### Dateオブジェクトの場合
```javascript
// 「日付をフォーマットする」の部分を関数化した
const formatDate = require('./formatDate');

const DATE = '2020-05-26T14:11:06';

const obj = new Date(DATE);
const formatted = formatDate(obj);

console.log(formatted);
// 2020/05/26 14時11分06秒
```
2020-05-26T14:11:06 のようなISOに準拠したフォーマットであればDateコンストラクタによってパースし、日付を解釈することができるのでDateオブジェクトでも苦労することはない。

しかし、MDNのDateの解説ページには以下のような記述がある。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date
>注: Date コンストラクター (または同等の Date.parse) で日付文字列を解釈することは、ブラウザーごとに動作が異なり一貫性がないため、避けるように強くすすめます。

Date.parse()のページにはさらに細かく書かれており、ブラウザが対応しているECMAスクリプトのバージョンによって同じ文字列でも違う日時と解釈されてしまうケースがあるらしい。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/parse

Node.jsで動くスクリプトならまだしも、ブラウザ上で動作するスクリプトの場合はDateコンストラクタによる日付文字列のパースは使用しない方が無難らしい。

### moment.js
```javascript
const moment = require('moment');

const FORMAT = 'YYYY/MM/DD HH時mm分ss秒';
const DATE = '2020-05-26T14:11:06';

const formatted = moment(DATE).format(FORMAT);

console.log(formatted);
// 2020/05/26 14時11分06秒
```
やはり書きやすく、読みやすい。

## 取得した日付（独自フォーマット）を別のフォーマットに変換したい
上と逆の場合を考える。
スプレッドシートから取り込んだような 2020/05/26 14時11分06秒 のような形式の日付をISO 8601形式に変換する。

### Dateオブジェクトの場合
正規表現で数値部分を抽出して年、月、日...を個別に取得し、DateオブジェクトにsetFullYear(),setDate()...を使ってセットして組み立てていく方法はある。
しかし、これならもはや自分でパースする関数を作った方が早くDateオブジェクトを通す意味が薄い。

もっとイケてる実装はあるように思うが、いずれにせよモジュールの製造が必要になる。

### moment.jsの場合
```javascript
const moment = require('moment');

const DATE = '2020/05/26 14時11分06秒';
const INPUT_FORMAT = 'YYYY/MM/DD HH時mm分ss秒';

const formatted = moment(DATE, INPUT_FORMAT).format();

console.log(formatted);
// 2020-05-26T14:11:06+09:00
```
入力日付のフォーマットを渡してやれば、独自フォーマットでも解釈することができる。
moment().format()は引数無しでISO 8601形式に変換される。
柔軟かつ、楽


## Dateで苦労するところまとめ
- 独自に定義した日付フォーマットの場合、Date単独では入力も出力もできない
- タイムゾーンは指定できるが、少し冗長になり慣れない人にはわかりづらい
- Dateコンストラクタでの日付文字列解釈は環境に依存するため非推奨
- getMonth()が0から始まるなど、直感的に扱えない部分がある