LoginSignup
22
17

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-08-23

初めに

この記事は、下記リンク先に投稿した 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 拡張形式」を再定義している為、どちらか一方を読むだけでも良いかもしれません。

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
- 20.3.3.2 Date.parse ( string ) - ECMAScript® 2017 Language Specification

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

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

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

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

22
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
17