[JavaScript] JavaScriptのDateとmoment.js

2017年3月6日月曜日

JavaScript moment.js

JavaScriptのDate

JavaScriptのDateオブジェクトは日付指定生成時に間違えやすい。
例えば、以下のようにすると
Console.log(new Date('2017/01/01'));
Console.log(new Date('2017-01-01'));
Console.log(new Date(2017, 1, 1));
Console.log(new Date(2017, 0, 1));

※検証環境: IE11
[date] Fri Jan 01 2017 00:00:00 GMT+0900 (東京 (標準時))
[date] Fri Jan 01 2017 09:00:00 GMT+0900 (東京 (標準時))
[date] Mon Feb 01 2017 00:00:00 GMT+0900 (東京 (標準時))
[date] Fri Jan 01 2017 00:00:00 GMT+0900 (東京 (標準時))

'YYYY-MM-DD'形式だけ、時刻が9時間ずれている!
※ちなみに文字列ではなく、(年, 月, 日)をそれぞれ数値で指定する形式では月が0始まりになっているので、new Date(2017, 1, 1)だと1ヶ月ずれてしまう。

これは'YYYY/MM/DD'形式だと、ローカル時刻と解釈されて、'YYYY-MM-DD'形式だと、協定世界時(UTC)と解釈されてしまうからだ。

なんで、'YYYY-MM-DD'形式がUTCと解釈されてしまうかというと、以下の理由による。

ISO8601

まず、ハイフンで年月日を区切る方式はISO8601で規定されている形式ということになる。
さらにISO8601でローカル時刻を表すには時刻の後ろにオフセットを付ける。
例えば、日本標準時(JST)なら、オフセットが+9時間で、こんな感じのフォーマットになる。

2017-01-01T12:34:56+09:00

協定世界時(UTC)の時刻を表したい場合はオフセットに'Z'を付ける。

2017-01-01T12:34:56Z

この辺の概要はWikipediaとかを参照。
https://ja.wikipedia.org/wiki/ISO_8601

じゃあ、時刻の後ろにオフセットが付いていない場合はどう解釈されるかって話になるんだけど、この辺はちゃんとした決まりがないんじゃないかな。
えーと、多分ない。

ので、'2017-01-01'のパース時にはUTCと解釈して、内部的には'2017-01-01T00:00:00Z'となる。
Console.log出力時には、それをローカル時刻に文字列変換するため、出力時には'2017-01-01T00:00:00+09:00'となる。

さらに動作環境による違いや、JavaScriptの仕様のバージョンによって違ったりする可能性があるので、Dateをそのまま使うのはけっこうしんどい。

日時の文字列をDateコンストラクタに渡した場合の挙動はDate.parseメソッドによる解釈と同じになるようなんだけど、MDNによると

MDN 標準ビルトインオブジェクト Date
注記: ブラウザごとに動作が異なり一貫性がないため、Date コンストラクタ (または同等の Date.parse) で日付文字列を解釈しないように強くすすめます。

ちょっと、おい。という感じではあるけど、まあ仕方ない。

moment.js

そこで、ちょっと機能的に物足りないDateの代わりにmoment.jsというライブラリを使ってみよう。

http://momentjs.com/
ライセンス:MIT license

さっき、Dateでやったことをmomentでやってみるとこんな感じになる。
console.log(moment('2017/01/01').format());
console.log(moment('2017-01-01').format());
console.log(moment([ 2017, 1, 1 ]).format());
console.log(moment([ 2017, 0, 1 ]).format());
2017-01-01T00:00:00+09:00
2017-01-01T00:00:00+09:00
2017-02-01T00:00:00+09:00
2017-01-01T00:00:00+09:00

momentの場合は'2017-01-01'をローカル時刻と解釈している。
また、年月日を指定する場合は配列で渡す。月が0始まりなのはDateと変わらないみたい。

具体的に書式を指定してみよう。3番目の引数をtrueにすると、より厳格(strict)なモードになる。
console.log(moment('2017/01/01', 'YYYY/MM/DD', true).format());
// 2017-01-01T00:00:00+09:00
console.log(moment('2017-01-01', 'YYYY-MM-DD', true).format());
// 2017-01-01T00:00:00+09:00

もちろん、format時にも書式を指定できる。
console.log(moment('2017-01-01', 'YYYY-MM-DD', true).format('YYYY/MM/DD'));
// 2017/01/01

日時をUTCとして解釈させたい場合はutcメソッドを使う。
console.log(moment.utc('2017-01-01').format());
// 2017-01-01T00:00:00Z

日付時間計算

日付や時間の計算もできる。
var d= moment('2017-01-01', 'YYYY-MM-DD', true);
d.add(1, 'day');
console.log(d.format());
// 2017-01-02T00:00:00+09:00

ちょっと注意が必要なのはmomentオブジェクトはミュータブルなので、addやsubtractをすると自オブジェクトの値が書き換わってしまうところ。
これが嫌な時はcloneメソッドを使おう。
var d1 = moment('2017-01-01', 'YYYY-MM-DD', true);
var d2 = d1.clone().subtract(1, 'day');
console.log(d1.format());
// 2017-01-01T00:00:00+09:00
console.log(d2.format());
// 2016-12-31T00:00:00+09:00