ECMAScript 2017 に準拠した方法で Date オブジェクトをシリアライズ(文字列化)する

# 初めに

この記事は、下記リンク先に投稿した `serialize-date-object-to-json.md` の複製版です。
記事後半で出てくる `JSONForDate` は下記リンク先からDL出来るライブラリの為、必要に応じて参照して下さい。

- [json-for-date.js: DateオブジェクトをJSON拡張形式にシリアライズ/パースの相互変換](https://gist.github.com/think49/26fcddaaa63cd0d6b27de8ef9c514c41)

# ECMAScript 2017

この記事では、ECMAScript 2017 に則った方法を紹介しています。
ECMAScript 2017 は JavaScript の根幹となる機能をまとめた標準仕様です。
この仕様に則れば、どの実装(ブラウザ)であっても同じように動作する事が期待できます。

# ISO 8601 拡張形式 (RFC3339)

ECMAScript 2017 では、日付と認識可能な文字列として **ISO 8601 拡張形式** を定義しています。
ISO 8601 拡張形式は曖昧な部分がある為、**RFC3339** でも再定義しているようです。
下記にそれぞれのリンク先を示しますが、ECMAScript 2017 でも「ISO 8601 拡張形式」を再定義している為、どちらか一方を読むだけでも良いかもしれません。

- [20.3.1.16 Date Time String Format - ECMAScript® 2017 Language Specification](http://www.ecma-international.org/ecma-262/8.0/#sec-date-time-string-format)
- [RFC3339 インターネット上の日付と時間：タイムスタンプ](http://www5d.biglobe.ne.jp/stssk/rfc/rfc3339j.html)

ECMAScript 2017 では **`YYYY-MM-DDTHH:mm:ss.sssZ`** もしくは **`YYYY-MM-DDTHH:mm:ss.sss[+-]HH:mm`** の形式として「ISO 8601 拡張形式」を定義しています。

- **YYYY** … グレゴリオ暦の0000〜9999桁の10進数。
- **MM** … 01（1月）から12（12月）までの年の月。
- **DD** … 01から31までの月の日。
- **T** … 時間要素の始まりを示す文字(訳注: Time の "T" と思われます)
- **HH** … 深夜0時から24侍を表した、00から24までの2桁の10進数(24時間法)
- **:** 「時間:分:秒」を区切る為の区切り文字(セパレータ)
- **mm** … 何時何分における何分を表す00から59までの2桁の10進数
- **ss** … 0分から1分までの間に存在する2桁の10進数00から59からなる秒数
- **.** … 「秒.ミリ秒」を区切る為の区切り文字(セパレータ)
- **sss** 3桁の10進数に構成されるミリ秒
- **Z** … UTC日時の場合、文字列の終端を表す(タイムゾーン指定子)
- **[+-]HH:mm** … タイムゾーンとなるUTCからの時差を表す。"+09:00" はUTCから「+9時間」の時差を表し、"-01:00" はUTCから「-1時間」の時差を表す(タイムゾーン指定子)

他にも次のルールがあります。

- ゼロパディングが必須です。`"2017-01-01"` を `"2017-1-1"` のようにゼロを切り詰めて表現する事は出来ません(MUST)。
- タームゾーン識別子が省略された場合、UTC として扱われます。
- 後方にある値は一部省略する事が可能です。省略された値は最も小さな値として扱われます。

省略規則はやや特殊な為、コード事例をあげます。

```JavaScript
new Date("2017-08-23T12:00:00.000+09:00").toISOString();  // "2017-08-23T03:00:00.000Z"
new Date("2017-08-23T12:00:00.000").toISOString();        // "2017-08-23T03:00:00.000Z"
new Date("2017-08-23T12:00:00").toISOString();            // "2017-08-23T03:00:00.000Z"
new Date("2017-08-23T12:00").toISOString();               // "2017-08-23T03:00:00.000Z"
new Date("2017-08-23T12").toISOString();                  // RangeError: Invalid time value
new Date("2017-08-23").toISOString();                     // "2017-08-23T00:00:00.000Z"
new Date("2017-08").toISOString();                        // "2017-08-01T00:00:00.000Z"
new Date("2017").toISOString();                           // "2017-01-01T00:00:00.000Z"
```

`HH:mm` から `HH` に省略できない事を除いて、後ろの要素を省略可能な事が分かります。

# new Date( value )

- [20.3.2.2 Date( value ) - ECMAScript® 2017 Language Specification](http://www.ecma-international.org/ecma-262/8.0/#sec-date-value)

`new Date()` は最も基本となる日付文字列用のパーサ(構文解析器)であり、ISO 8061 拡張形式の文字列を `Date` オブジェクトに変換することが出来ます。

- [Date - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date)
- [Date.prototype.toISOString() - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)

```JavaScript
var dateString1 = '2017-08-23T03:00:00.000Z',
    dateString2 = '2017-08-23T12:00:00.000+09:00';

console.log(new Date(dateString1)); // Wed Aug 23 2017 12:00:00 GMT+0900 (東京 (標準時))
console.log(new Date(dateString2)); // Wed Aug 23 2017 12:00:00 GMT+0900 (東京 (標準時))
```

<<<<<<< HEAD
# Date.parse( string )

=======
>>>>>>> EDIT_REQUEST
- [20.3.3.2 Date.parse ( string ) - ECMAScript® 2017 Language Specification](http://www.ecma-international.org/ecma-262/8.0/#sec-date.parse)

`Date.parse()` も`new Date` と同様、ISO 8061 拡張形式の文字列を、パース(構文解析)出来ますが、次の点が異なります。

- `Date.parse()` は日付文字列しか引数にとれない(`new Date` には Number 型の値を引数にとる等、他の機能がある)
- `Date.parse()` は協定世界時(UTC)からの経過ミリ秒数を返す (`new Date` は `Date` オブジェクトを返す)

コードを書いてみましょう。

```JavaScript
var dateString1 = '2017-08-23T03:00:00.000Z',
    dateString2 = '2017-08-23T12:00:00.000+09:00';

console.log(Date.parse(dateString1)); // 1503457200000
console.log(Date.parse(dateString2)); // 1503457200000

console.log(new Date(Date.parse(dateString1))); // Wed Aug 23 2017 12:00:00 GMT+0900 (東京 (標準時))
console.log(new Date(Date.parse(dateString2))); // Wed Aug 23 2017 12:00:00 GMT+0900 (東京 (標準時))
```

`Date` オブジェクトに変換する為には `Date.parse` でUTCからのミリ秒を得た後に `new Date` を更に実行しなければなりません。
`new Date` でも「ISO 8061 拡張形式」を扱える為、この場合は冗長なコードとなっています。

# Date.prototype.toISOString

- [20.3.4.36 Date.prototype.toISOString() - ECMAScript® 2017 Language Specification](http://www.ecma-international.org/ecma-262/8.0/#sec-date.prototype.toisostring)

`Date.prototype.toISOString` は `Date` オブジェクトを「ISO 8061 拡張形式」の**UTC文字列**にシリアライズ(文字列化)します。

```JavaScript
var date = new Date("2017-08-23T12:00:00.000+09:00");

console.log(date);                // Wed Aug 23 2017 12:00:00 GMT+0900 (東京 (標準時))
console.log(date.toISOString());  // 2017-08-23T03:00:00.000Z
```

# Date.prototype.toJSON( key )

- [20.3.4.37 Date.prototype.toJSON ( key ) - ECMAScript® 2017 Language Specification](http://www.ecma-international.org/ecma-262/8.0/#sec-date.prototype.tojson)

`Date.prototype.toJSON` は `this` 値を Number 型に変換した値が有限数であった場合、`Date.prototype.toISOString` を呼び出し、その返り値をそのまま返します。
つまり、`Date.prototype.toISOString` と同じ実行結果を返します。
(※このメソッドは **JSON.stringify() で Date オブジェクトをシリアライズ(文字列化)する為に定義**されており、通常は明示的に呼び出す事はありません。)

```JavaScript
var date = new Date("2017-08-23T12:00:00.000+09:00");

console.log(date);           // Wed Aug 23 2017 12:00:00 GMT+0900 (東京 (標準時))
console.log(date.toJSON());  // 2017-08-23T03:00:00.000Z
```

# JSON.stringify( value [ , replacer [ , space ] ] )

- [24.5.2 JSON.stringify ( value [ , replacer [ , space ] ] ) - ECMAScript® 2017 Language Specification](http://www.ecma-international.org/ecma-262/8.0/#sec-json.stringify)

`JSON.stringify()` はJSONをシリアライズ(文字列化)する関数です。
`JSON.stringify()` は対象のオブジェクトに `toJSON` という名前のプロパティが存在し、それが関数であったならば、`toJSON()` を呼び出してシリアライズ(文字列化)します。

```JavaScript
var date = new Date("2017-08-23T12:00:00.000+09:00");

console.log(date);                  // Wed Aug 23 2017 12:00:00 GMT+0900 (東京 (標準時))
console.log(JSON.stringify(date));  // "2017-08-23T03:00:00.000Z"
```

`Date.prototype.toJSON` は `Date.prototype.toISOString` と同じ処理になるので、`JSON.stringify()` でシリアライズする事は「ISO 8061 拡張形式」にシリアライズする事と同義です。
ただし、JSON は文字列にシリアライズする際に文字列リテラルの形式にする為、前後に `"` (ダブルコーテーション)が付与される事になります。

# JSON.parse( text [ , reviver ] )

- [24.5.1 JSON.parse ( text [ , reviver ] ) - ECMAScript® 2017 Language Specification](http://www.ecma-international.org/ecma-262/8.0/#sec-json.parse)

`JSON.parse()` は `JSON.stringify()` によってシリアライズされた文字列を元の形に戻す関数です。
しかし、残念ながら、`JSON.parse()` は「`JSON.stringify()` によって `Date` オブジェクトから変換された ISO 8061 拡張形式の文字列」を String 型のデータと判断する為、`JSON.stringify()` によるシリアライズは**不可逆**となります。

```JavaScript
var date = new Date("2017-08-23T12:00:00.000+09:00"),
    json = JSON.stringify(date);

console.log(date);              // Wed Aug 23 2017 12:00:00 GMT+0900 (東京 (標準時))
console.log(json);              // "2017-08-23T03:00:00.000Z"
console.log(JSON.parse(json));  // 2017-08-23T03:00:00.000Z
```

# JSON にシリアライズされた日付文字列を組み込む場合

## 不可逆性の問題

前述の通り、`Date` オブジェクトのシリアライズ/パースにおいて、JSONの活用は不可逆です。
そんな状況を考慮してか、`JSON.stringify()`, `JSON.parse()` にはコールバック関数を引数にとる事で出力値を変更する機能があります。

- `JSON.stringify()` は第二引数に replacer となる関数を与える事で出力値を変更できる
- `JSON.parse()` は第二引数に reviver となる関数を与える事で出力値を変更できる

まず、`JSON.parse()` から実装してみましょう。

```JavaScript
'use strict';
function reviver (key, value) {
  return /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/.test(value) ? new Date(value) : value;
}

var date = new Date('2017-08-22T09:00:00+09:00'),
    array1 = [date, date.toISOString()],
    json = JSON.stringify(array1),
    array2 = JSON.parse(json, reviver);

console.log(array1);  // [Tue Aug 22 2017 09:00:00 GMT+0900 (東京 (標準時)), "2017-08-22T00:00:00.000Z"]
console.log(array2);  // [Tue Aug 22 2017 09:00:00 GMT+0900 (東京 (標準時)), Tue Aug 22 2017 09:00:00 GMT+0900 (東京 (標準時))]
console.log(json);    // ["2017-08-22T00:00:00.000Z","2017-08-22T00:00:00.000Z"]
```

期待通り、`Date` オブジェクトに戻す事が出来ました。
しかし、このままでは元々、文字列として存在した `"2017-08-22T00:00:00.000Z"` も `Date` オブジェクトに変換されてしまうので、`JSON.stringify()` にもコールバック関数を与えてみましょう。

```JavaScript
'use strict';
function reviver (key, value) {
  if(/date:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/.test(value)) { // new Date なら
    return new Date(value);
  }

  if(/string:\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/.test(value)) {  // String 型なら
    return value.slice(7);
  }

  return value;
}

function replacer (key, value) {
  if (Object(value) === value && Object.getPrototypeOf(value) === Date.prototype) { // new Date なら
    return 'date:' + date.toISOString();
  }

  if (typeof value === 'string') {  // String 型なら
    return 'string:' + value;
  }

  return value;
}

var date = new Date('2017-08-22T09:00:00+09:00'),
    array1 = [date, date.toISOString()],
    json = JSON.stringify(array1, replacer),
    array2 = JSON.parse(json, reviver);

console.log(array1);  // [Tue Aug 22 2017 09:00:00 GMT+0900 (東京 (標準時)), "2017-08-22T00:00:00.000Z"]
console.log(array2);  // ["2017-08-22T00:00:00.000Z", "2017-08-22T00:00:00.000Z"]
console.log(json);    // ["string:2017-08-22T00:00:00.000Z","string:2017-08-22T00:00:00.000Z"]
```

期待に反して、両方とも「文字列」として扱われてしまいました。
なぜなら、**replacer() を通した時点で Date オブジェクトは既に「ISO 8061拡張形式」の文字列に変換されてしまっているから**です。

これでは `replacer()` による変換は諦めるしかなく、別の切り口でシリアライズする方法を考える必要があります。

## (解決策) JSON.stringify 実行前に Date オブジェクトと文字列を衝突しない値に書き換える

やや強引ですが、事前に全ての `Date` オブジェクト、`String` 値に対して衝突しない値に書き換えてやれば、両者を区別することが出来ます。
`Date` オブジェクトは `toJSON` プロパティを書き換える事で `"date:"` の接頭辞付きで出力するものとし、`String` 値には `"string:"` の接頭辞を付けます。
パース処理には `JSON.parse()` の第二引数を利用します。

- [json-for-date.js: DateオブジェクトをJSON拡張形式にシリアライズ/パースの相互変換 - Gist](https://gist.github.com/think49/26fcddaaa63cd0d6b27de8ef9c514c41)

```JavaScript
var date = new Date("2017-08-23T12:00:00.000+09:00"),
    array = [date, date.toISOString(), [1,2], {a: 'foo', b: 'bar'}],
    json = JSONForDate.stringify(array);

console.log(json);  // ["date:2017-08-23T03:00:00.000Z","string:2017-08-23T03:00:00.000Z",[1,2],{"a":"string:foo","b":"string:bar"}]
console.log(JSONForDate.parse(json)); // [Wed Aug 23 2017 12:00:00 GMT+0900 (東京 (標準時)),"2017-08-23T03:00:00.000Z",[1,2],{"a":"foo","b":"bar"}]
```

`JSONForDate` の基本的な使い方は `JSON` と同じです。
詳細は下記リンク先にある `readme.md` を参照して下さい。

- [json-for-date.js: DateオブジェクトをJSON拡張形式にシリアライズ/パースの相互変換](https://gist.github.com/think49/26fcddaaa63cd0d6b27de8ef9c514c41/)