初めに
この記事は、下記リンク先に投稿した serialize-date-object-to-json.md
の複製版です。
記事後半で出てくる JSONForDate
は下記リンク先からDL出来るライブラリの為、必要に応じて参照して下さい。
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
- RFC3339 インターネット上の日付と時間:タイムスタンプ
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 として扱われます。
- 後方にある値は一部省略する事が可能です。省略された値は最も小さな値として扱われます。
省略規則はやや特殊な為、コード事例をあげます。
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 )
new Date()
は最も基本となる日付文字列用のパーサ(構文解析器)であり、ISO 8061 拡張形式の文字列を Date
オブジェクトに変換することが出来ます。
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
Date.parse()
もnew Date
と同様、ISO 8061 拡張形式の文字列を、パース(構文解析)出来ますが、次の点が異なります。
-
Date.parse()
は日付文字列しか引数にとれない(new Date
には Number 型の値を引数にとる等、他の機能がある) -
Date.parse()
は協定世界時(UTC)からの経過ミリ秒数を返す (new Date
はDate
オブジェクトを返す)
コードを書いてみましょう。
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
Date.prototype.toISOString
は Date
オブジェクトを「ISO 8061 拡張形式」のUTC文字列にシリアライズ(文字列化)します。
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 )
Date.prototype.toJSON
は this
値を Number 型に変換した値が有限数であった場合、Date.prototype.toISOString
を呼び出し、その返り値をそのまま返します。
つまり、Date.prototype.toISOString
と同じ実行結果を返します。
(※このメソッドは JSON.stringify() で Date オブジェクトをシリアライズ(文字列化)する為に定義されており、通常は明示的に呼び出す事はありません。)
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
JSON.stringify()
はJSONをシリアライズ(文字列化)する関数です。
JSON.stringify()
は対象のオブジェクトに toJSON
という名前のプロパティが存在し、それが関数であったならば、toJSON()
を呼び出してシリアライズ(文字列化)します。
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 ] )
JSON.parse()
は JSON.stringify()
によってシリアライズされた文字列を元の形に戻す関数です。
しかし、残念ながら、JSON.parse()
は「JSON.stringify()
によって Date
オブジェクトから変換された ISO 8061 拡張形式の文字列」を String 型のデータと判断する為、JSON.stringify()
によるシリアライズは不可逆となります。
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()
から実装してみましょう。
'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()
にもコールバック関数を与えてみましょう。
'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()
の第二引数を利用します。
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
を参照して下さい。