MessagePackフォーマット仕様にTimestamp型を追加

MessagePackフォーマット仕様のPull Request #209をマージし、MessagePackにTimestamp型を追加しました。

※この記事の英語版は XXX にあります(翻訳中)

Extension型の型コード -1 として定義されているため、後方互換性が維持されています。つまり、既にExtension型に対応しているデシリアライザであれば、Timestamp型を使用して作成されたデータを、Timestamp型に対応していない古いデシリアライズで読み出すことができます。

新しいTimestamp型には timestamp 32、timestamp 64、timestamp 96 の3つのフォーマットがあり、よく使う値をより少ないバイト数で保存できるようになっています。例えば、1970年〜2106年までの時刻で、秒までの精度しか持たない時刻であれば、合計6バイトで保存できます。1970年〜2514年までの時刻であれば、ナノ秒精度まで含めて10バイト。それを超える場合のみ、-584554047284年〜584554051223年までのナノ秒精度の時刻を15バイトで保存できます。

仕様にはシリアライズ・デシリアライズを行う簡単なコードなコードをも付属しています。Javaによる実装は msgpack/msgpack-java#431 のようになるでしょう。

ライブラリの実装では、2013年にRaw型からBinary型とString型を分離したケースと同様に、Timestamp型のデシリアライズにマイナーバージョンアップで対応し、Timestamp型のシリアライズをデフォルトの挙動として行う対応をメジャーバージョンアップで対応する方法が推奨されます。

さて、Timestamp型の追加については、次の3つの懸念点がありました。ここでは仕様を決めた背景について書きたいと思います。

タイムゾーン

MessagePackのTimestamp型は、タイムゾーンに関する情報を保存しません。常に1970年1月1日 00:00:00 UTCからの経過秒を保存することになります。

まず「時刻」という概念を整理すると、次のように分類することができます*1

  • 概念時刻(Local Time)
  • 絶対時刻(Instant)
    • UNIX時刻 例:2017-08-10T01:25:00Z
    • 概念時刻 + オフセット 例:2017-08-09 18:25:00 -0700
    • 概念時刻 + タイムゾーン名 例:2017-08-09 18:25:00 America/Los_Angeles

概念時刻は、世界のどこのタイムゾーンで扱うかによって別の瞬間を示します。例えば 2017-08-09 18:25:00 は、どのタイムゾーンで解釈するかによって実際の時刻が変わるため、概念時刻です。概念時刻は、理論上は暦(calendar system)に依存していくつかの表現方法がありますが、普通はグレゴリオ暦(西暦)を使います。

絶対時刻は、世界中のどこで扱っても、同じ一瞬の時間を示します。例えば 2017-08-09 18:25:00 -0700 は、絶対時刻です。絶対時刻にもいくつかの表現方法があり、それぞれに利害得失があります。

UNIX時刻はもっともシンプルな方法で、タイムゾーンや暦に依存せず、常に1970年1月1日 00:00:00.000 UTCからの経過時間で時刻を表現します。タイムゾーンに関する情報は保存しません。

概念時刻にオフセットを加える方法は、年月日・時分秒を表示してくれるので、人間が見て分かりやすいという利点があります。

概念時刻にタイムゾーン名を加える方法は、「1日後」や「1週間前」といった暦に基づいた時刻の計算が簡単にできるという利点があります。例えば、米国の太平洋時間では、2017-03-12 00:00:00 -0800 の1日後は 2017-03-13 00:00:00 -0700 です。この間には夏時間の切り替えがあるため、オフセットが変化します。暦の上では1日後ですが、24時間後ではなく23時間後です。一方で、タイムゾーン名は夏時間の切り替えにかかわらず、太平洋時間を採用する地域では America/Los_Angeles です。つまり2017-03-12 00:00:00 America/Los_Angeles の1日後は、単に 2017-03-13 00:00:00 America/Los_Angelesです。一方で、絶対時間を得るためには、タイムゾーンの名前から意味(いつから夏時間が始まるか等)を引く必要があります。このためのデータベースはTime Zone Databaseと呼ばれ、IANAで管理されており、たまに変わります。最近では、ハイチ共和国が2017年に夏時間の慣習を変更したため、データベースが更新されました。

言語やデータベースシステムの型システムでは、次のように表現されています:

  • MySQL
    • DATETIME: 概念時刻
    • TIMESTAMP: 絶対時刻
  • PostgreSQL
    • timestamp: 概念時刻
    • timestamptz: 絶対時刻(ストレージ上:UNIX時刻、SQL結果セット上:概念時刻 + オフセット)
  • Java
    • java.time.LocalDateTime: 概念時刻
    • java.time.Instant: 絶対時刻(UNIX時刻)
    • java.time.OffsetDateTime: 絶対時刻(概念時刻 + オフセット)
    • java.time.ZonedDateTime: 絶対時刻(概念時刻 + タイムゾーン名)
  • Go
  • Ruby
  • Python
  • C (time.h: C99 + TC2)
    • struct timespec: 絶対時刻(UNIX時刻)
    • struct tm: 概念時刻

MessagePackでは、絶対時刻(UNIX時刻)を扱います。

タイムゾーン情報を含めなかった理由は、MessagePackは機械が高速に効率良く扱えるフォーマットを目指しており、また人間に表示する場合でも、その人間がいる場所のタイムゾーン情報を表示する時に付与するという方法で、表示上の課題はおおよそ解決できると考えたからです。タイムゾーン情報が本当に必要であれば、タイムゾーン名を別のフィールドで保存することもできます。例えば次のAやBの代わりに、Cを使用すれば良いでしょう:

A (概念時刻 + オフセットをタイムスタンプの値ごとに保存):

"user": {
    "last_login": "2017-08-09T18:25:00-0700",
    "signup": "2017-03-12T00:00:00-0800"
}

B (概念時刻 + タイムゾーン名をタイムスタンプの値ごとに保存):

"user": {
    "last_login": "2017-08-09T18:25:00[America/Los_Angeles]",
    "signup": "2017-03-12T00:00:00[America/Los_Angeles]"
}

C (絶対時刻をUNIX時刻/UTCで保存、タイムゾーン名を別フィールドに保存):

"user": {
    "last_login": "2017-08-10T01:25:00Z",
    "signup": "2017-03-12T08:00:00Z",
    "timezone": "America/Los_Angeles"
}

精度

MessagePackのTimestamp型は、ナノ秒精度の時刻を扱います。マイクロ秒やピコ秒ではなくナノ秒にした理由は、いくつかの近年のプログラミング言語ナノ秒のサポートを追加しているためです。ピコ秒をサポートする言語はほとんど見つかりませんでした。

また、ナノ秒は32ビット以下(30ビット)でちょうど表現できる一方で、マイクロ秒は20ビットでこれは16ビットを超えており、ピコ秒の表現には40ビットも必要になるため、効率の上でもきりが良く有利です。

追記:Rubyの精度が間違っていたので修正しました。ピコ秒以下の精度をサポートするシステムもいくつかあるようです:

JSONとの互換性

JSONとMessagePackの両方に対応するアプリケーションは、比較的よくあるMessagePackのユースケースの一つです。JSONにはTimestamp型が無いため、MessagePackにおけるTimestamp型をJSONではどう表現するべきでしょうか?

JSONで時刻を表現する方法は標準化されていませんが、Github APIGithub GraphQL APIFacebook Graph APIなどを見ると、ISO-8601形式、つまり YYYY-MM-DD “T” hh:mm:ss “Z” 形式の表示が一般的になりつつあるように見えます。Open API 3.0.0(Swagger)でも、ISO-8601形式と互換性のあるRFC 3339形式を採用しています。

つまり、MessagePackのTimestamp型を使って値をシリアライズするケースで、JSONにも対応したい場合には、YYYY-MM-DD “T” hh:mm:ss “Z” 形式を使用すれば良いでしょう。

バージョニング

MessagePackの仕様にバージョンを付けようと思っています。 Timestamp型が追加される前のバージョンを2.0、追加された後のバージョンを2.1とし、過去のBinary型やExtension型が入る前のバージョンをさかのぼって1.0とする案です。GithubへのPull Requestはこちら:(作業中)

*1:「概念時刻」「絶対時刻」の用語は timestamp with time zone型はタイムゾーン情報を持っていない から取りました