Skip to content

Instantly share code, notes, and snippets.

@think49
Last active September 17, 2021 03:05
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save think49/26fcddaaa63cd0d6b27de8ef9c514c41 to your computer and use it in GitHub Desktop.
Save think49/26fcddaaa63cd0d6b27de8ef9c514c41 to your computer and use it in GitHub Desktop.
json-for-date.js: DateオブジェクトをJSON拡張形式にシリアライズ/パースの相互変換

json-for-date.js

概要

ECMAScript 2017 規定の JSON では Date オブジェクトをシリアライズ/パースで相互変換できない問題を解決します。

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

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

JSON.stringify()Date オブジェクトを「ISO 8061拡張形式(RFC3339)」に変換し、JSON.parse は両方とも文字列として扱う為、元の形式に戻すことが出来ません。

JSONForDate では両者の形式を別々の値に変換する事で「Date オブジェクト --- JSON」の相互変換を可能とします。

var date = new Date('2017-08-22T09:00:00+09:00'),
    array = [date, date.toISOString()],
    json = JSONForDate.stringify(array);

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

JSONForDate.stringify( value [ , replacer [ , space ] ] )

第一引数に与えられた値を拡張JSON形式にシリアライズ(文字列化)します。

console.log(JSONForDate.stringify(new Date('2017-08-22T09:00:00+09:00')));  // "date:2017-08-22T00:00:00.000Z"
console.log(JSONForDate.stringify('2017-08-22T09:00:00+09:00'));            // "string:2017-08-22T09:00:00+09:00"
console.log(JSONForDate.stringify(['foo', 'bar']));                         // ["string:foo","string:bar"]

JSON.stringify と同様、第二引数で関数を指定する事で出力されるJSON形式を変えることが出来ます。

var json = JSONForDate.stringify([1, 2, 3], function (key, value) {
  return typeof value === 'number' ? value * 2 : value;
});
console.log(json);  // [2,4,6]

JSON.stringify と同様、第三引数でインデントに使う空白文字を制御する事が可能です。

  • Number 型を指定 … 0-10の範囲で半角スペースの数として扱う
  • String 型を指定 … インデント文字として扱う(最初の10文字のみ)
var json = JSONForDate.stringify([1, 2, 3], function (key, value) {
  return value;
}, 2);
console.log(json);

次の値がコンソールに出力されます。

[
  1,
  2,
  3
]

JSONForDate.parse( text [ , reviver ] )

第一引数に与えられた拡張JSON形式の文字列をパース(構文解析)し、シリアライズ前の状態に戻します。

console.log(JSONForDate.parse('["date:2017-08-22T00:00:00.000Z","string:2017-08-22T00:00:00.000Z",[1,2,3],{"a":"string:foo","b":"string:bar"}]'));  // [Tue Aug 22 2017 09:00:00 GMT+0900 (東京 (標準時)), "2017-08-22T00:00:00.000Z", [1, 2, 3], {a: "foo", b: "bar"}]

JSON.parse() と同様、第二引数に関数を指定する事でパースされる値を書き換える事が出来ます。 第二引数のコールバック関数で受け取れるのは "string:" 等の接頭辞の削除処理が終わった後という点に注意して下さい。

var json = '["string:<<foo>>","string:<<bar>>","string:<<piyo>>"]',
    array = JSONForDate.parse(json, function (key, value) {
      return typeof value ==='string' ? value.replace(/^<<|>>$/g, '') : value;
    });

console.log(array); // console.log(JSON.parse(json));
/**
* json-for-date-1.0.2.js
* JSON which can serialize Date object
*
* @version 1.0.2
* @author think49
* @url https://gist.github.com/think49/26fcddaaa63cd0d6b27de8ef9c514c41
* @license http://www.opensource.org/licenses/mit-license.php (The MIT License)
*/
(function (Object, keys, getPrototypeOf, Date, _parse, _stringify, isArray) {
'use strict';
function toJSON () {
return 'date:' + this.toISOString();
}
function escapeDateAndString (target, source /* [,... source] */) {
if (typeof target === 'undefined' || target === null) {
throw new TypeError('Cannot convert undefined or null to object');
}
var len = arguments.length, i = 1;
if (typeof target === 'string') {
target = 'string:' + target;
}
target = Object(target);
if (getPrototypeOf(target) === Date.prototype) {
target = new Date(target);
target.toJSON = toJSON;
}
while (i < len) {
source = arguments[i++];
if (typeof source !== 'undefined' && source !== null) {
source = Object(source);
for (var j = 0, sKeys = keys(source), sLen = sKeys.length, key, value; j < sLen; ++j) {
key = sKeys[j];
value = source[key];
if (typeof value === 'string') {
target[key] = 'string:' + value;
} else if (getPrototypeOf(value) === Date.prototype) {
value = new Date(value);
value.toJSON = toJSON;
target[key] = value;
} else if (isArray(value)) {
target[key] = escapeDateAndString([], value);
} else {
target[key] = Object(value) === value ? escapeDateAndString({}, value) : value;
}
}
}
}
return target;
};
function reviver (key, value) {
if(/^date:/.test(value)) { // new Date
return new Date(value.slice(5));
}
if(/^string:/.test(value)) { // String Type
return value.slice(7);
}
return value;
}
Object.defineProperty(this, 'JSONForDate', {
writable: true,
enumerable: false,
configurable: true,
value: Object.create(Object.prototype, {
'parse': {
writable: true,
enumerable: false,
configurable: true,
value: function parse (string /* [, reviver] */) {
return arguments.length < 2 ? _parse(string, reviver) : _parse(string, function (key, value) {
return this(key, reviver(key, value));
}.bind(arguments[1]));
}
},
'stringify': {
writable: true,
enumerable: false,
configurable: true,
value: function stringify (value /* [, replacer [, space]] */) {
arguments[0] = isArray(value) && escapeDateAndString([], value) ||
getPrototypeOf(value) === Date.prototype && escapeDateAndString(new Date(value), value) ||
Object(value) === value && escapeDateAndString({}, value) ||
escapeDateAndString(value);
return _stringify.apply(null, arguments);
}
}})
});
}.call(this, Object, Object.keys, Object.getPrototypeOf, Date, JSON.parse, JSON.stringify, Array.isArray));

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

ECMAScript 2017

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

ISO 8601 拡張形式 (RFC3339)

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

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 (東京 (標準時))

- [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

Date.prototype.toISOStringDate オブジェクトを「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.toJSONthis 値を 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 ] ] )

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.toJSONDate.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 を参照して下さい。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment