JavaScript の Date は罠が多すぎる


JavaScript で日付・時間を扱っていて、次から次へと罠にはまったので、あとから来る人のために書き留めておく。

## Date.parse が返すのは Date でなく整数

`Date.parse` は、世界協定時 1970 年 1 月 1 日 00:00:00 からの経過時間をミリ秒単位で返す。
Date を得るには `new Date` に渡す。
new Date に直接文字列渡しても同じ挙動なので、こちらのが簡潔。

```javascript
msec = Date.parse("Thu, 06 Sep 2012 00:00:00 +0900"); // 1346857200000
date = new Date(msec); // Date
date = new Date("Thu, 06 Sep 2012 00:00:00 +0900"); // Date
```
## 年を得るのに getYear は使わない

`Date.prototype.getYear` は 1900 年からの年数で非推奨。
`Date.prototype.getFullYear` を使う。

```javascript
date = new Date("Thu, 06 Sep 2012 00:00:00 +0900")
date.getYear(); // 112 (2012 - 1900)
date.getFullYear(); // 2012
```

## 月は 0 から始まる

`new Date` の第 2 引数でも、`Date.prototype.getMonth` でも、月は 0-11で表す。

```javascript
new Date(2012, 8, 6); // Thu, Sep 06 2012 00:00:00 GMT+0900 (JST)
new Date("Thu, 06 Sep 2012 00:00:00 +0900").getMonth();  // 8
```

## 日を得るのは getDay ではない

`Date.prototype.getDay` は 0 を日曜とする曜日を返す。
日にちは `Date.prototype.getDate`。
月とは違いこちらは 1-31。

```javascript
date = new Date("Thu, 06 Sep 2012 00:00:00 +0900");
date.getDay(); // 4 (木曜日)
date.getDate(); // 6 (6日)
```

## getTimezoneOffset の符号は普通の表記と逆

`Date.prototype.getTimezoneOffset` は UTC からの時差を分で返すが、通常タイムゾーンを示す文字列表現とは符号が逆。

```javascript
date = new Date("Thu, 06 Sep 2012 00:00:00 +0900");
date.getTimezoneOffset(); // -540 (540分=9時間で +0900 のこと)
```

## 比較するときは getTime 使う

同じ日時を示す Date でもオブジェクトが異なれば == や === は false になる。

```javascript
s = "Thu, 06 Sep 2012 00:00:00 +0900"
new Date(s) == new Date(s) // false
new Date(s) === new Date(s) // false
```

`Date.prototype.getTime` で 世界協定時 1970 年 1 月 1 日 00:00:00 からの経過時間をミリ秒単位で得られるのでこれで比較する。

```javascript
s = "Thu, 06 Sep 2012 00:00:00 +0900"
new Date(s).getTime() // 1346857200000
new Date(s).getTime() == new Date(s).getTime() // true
new Date(s).getTime() === new Date(s).getTime() // true
```

ちなみに Date 同士の比較でも `<` や `>` は期待通りに動作するが、ややこしいのでおすすめしない。

(減算したり単項演算子 + を使って数値にキャストして比較する方法もあるが、意図がわかりにくい上に[遅い](http://jsperf.com/comparing-date-objects)ので使う理由はない)

## 文字列表現は toISOString か toJSON で取得する

ECMAScript 3 で定義されている、文字列表現を得るメソッドには、下記のものがある (括弧内は [EMCAScript 3 仕様書 (PDF)](http://www.ecma-international.org/publications/files/ECMA-ST-ARCH/ECMA-262,%203rd%20edition,%20December%201999.pdf) の項番号)。

- toString (15.9.5.2)
- toDateString (15.9.5.3)
- toTimeString (15.9.5.4)
- toLocaleString (15.9.5.5)
- toLocaleDateString (15.9.5.6)
- toLocaleTimeString (15.9.5.7)

が、いずれも "The contents of the string are implementation-dependent" と書かれており、形式は実装依存。

試してみたら違うのは IE だけだったけど、規格がないので責めてはいけない。

```javascript
date = new Date("Thu, 06 Sep 2012 00:00:00 +0900");
date.toString() // Chrome21:  "Thu Sep 06 2012 00:00:00 GMT+0900 (JST)"
date.toString() // Firefox14: "Thu Sep 06 2012 00:00:00 GMT+0900 (JST)"
date.toString() // Safari:    "Thu Sep 06 2012 00:00:00 GMT+0900 (JST)"
date.toString() // IE8:       "Thu Sep 6 00:00:00 UTC+0900 2012"
```

ECMAScript 5 では文字列表現を[ISO8601 のサブセットに規定](http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15)しており [toISOString](http://ecma-international.org/ecma-262/5.1/#sec-15.9.5.43) で得られる。

```javascript
date = new Date("Thu, 06 Sep 2012 00:00:00 +0900");
date.toISOString(); // "2012-09-05T15:00:00.000Z"
```

`JSON.stringify` から呼ばれる [toJSON](http://ecma-international.org/ecma-262/5.1/#sec-15.9.5.44) も toISOString を使うので、JSON.stringify したときも ISO8601 になる。

```javascript
date = new Date("Thu, 06 Sep 2012 00:00:00 +0900");
JSON.stringify(date); // ""2012-09-05T15:00:00.000Z""
```


また、[Date.parse](http://ecma-international.org/ecma-262/5.1/#sec-15.9.4.2) や [new Date](http://ecma-international.org/ecma-262/5.1/#sec-15.9.3.2) でも、ISO8601 (のサブセット) をパースできることを定めている。

```javascript
new Date("2012-09-05T15:00:00.000Z") // Thu Sep 06 2012 00:00:00 GMT+0900 (JST)
```


## JSON にすると日付が変わることがある

上で述べたように JSON.stringify したときの文字列表現は ISO8601 になるが、タイムゾーンを無視して[常に UTC になってしまう](http://ecma-international.org/ecma-262/5.1/#sec-15.9.5.43)。

これにより、タイムゾーンと時刻によっては日付が変わる。

```javascript
// 日本時間で 6 日 0 時は UTC では 5 日 15 時
date = new Date("2012-09-06T00:00:00+0900");
JSON.stringify(date) // "2012-09-05T15:00:00.000Z"
```

日付を維持したい場合、タイムゾーンの情報を保持したい場合は、JSON.stringify する前に文字列等に変換しておく必要がある。

## 日付をパースした場合と数値で指定した場合で時刻が一致しない

new Date や Date.parse で時刻を含まない日付文字列をパースすると、UTC 00:00:00 になる。

```javascript
// UTC 0 時 = JST 午前 9 時
new Date("2014-11-10") // Mon Nov 10 2014 09:00:00 GMT+0900 (JST)
```

一方で `new Date(2014, 10, 10)` などとしたときは地方時 00:00:00 となり、これと時刻が一致しない。

```javascript
new Date(2014, 10, 10) // Mon Nov 10 2014 00:00:00 GMT+0900 (JST)
```

前者の挙動は、日付文字列でタイムゾーン文字列がない場合、UTC として扱うという ECMAScript 5 の仕様に基づく。

> The value of an absent time zone offset is “Z”.
> [15.9.1.15 Date Time String Format](http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15)

ただし、ECMAScript 6 draft (October 14, 2014) では、これを地方時にするとの記述がある。

> If the time zone offset is absent, the date-time is interpreted as a local time.
> [20.3.1.15 Date Time String Format](http://people.mozilla.org/~jorendorff/es6-draft.html#sec-date-time-string-format)


## ISO8601 は IE8 ではパースできない

Date.parse がパースできる形式は ECMAScript 3 では定められていないため実装依存。

試してみたら、主要ブラウザでは RFC2822 なら OK、ISO8601 は IE8 ではだめ (IE9 は OK)。

```javascript
Date.parse("2012-01-01 00:00:00"); // IE8 だと NaN
```

上で述べたように ECMAScript 5 では ISO8601 (のサブセット) をパースできるよう定めている。