私は日付時刻の処理が大好きです。タイムゾーンの問題でデータ抽出が9時間分漏れていたとか、朝9時の始業前のログが昨日付けになってしまっていたなんていう問題が起こると喜んじゃうタイプ。
そんな私にとって、各プログラミング言語が標準で持っている日付時刻型クラスにはそれぞれ思うところがあり、今日はちょっとその品評会をしてみたいと思います。
エムスリーエンジニアリンググループ、Unit1(製薬企業向けプラットフォームチーム)三浦(@yuba@reax.work) [記事一覧]がお送りいたします、エムスリー Advent Calendar 2023の2日目です。
各言語の不満点をつらつら並べていくのかと思わせておいていきなりですが、至高の言語、ほぼ不満のない言語を紹介します。
BigQueryは、このテックブログでもひんぱんに言及されているGoogle提供のビッグデータ分析用データベースですね。この問い合わせに使うSQL言語が理想的な日付時刻型データ体系をそなえています。
理想的というからにはいかなる体系か、それは次の4つの型からなっています。
お気付きでしょうか、タイムゾーンを持っている型というのが一つもないのです。
国際的に使われるアプリケーション、それこそWebアプリなどを作るのにタイムゾーン情報って日付時刻型に必要じゃないんですか? はい、まったく必要じゃありませんというのが今回私のお伝えしたいことになります。
次の相関図を見てください。
タイムゾーンはどこに出てきますか? そう、データの一部ではなくて、データを変換するときに添えるものなのです。
「日付時刻2023-12-02 12:00:00
は、東京時間(Asia/Tokyo)でのことなのならこれこれの瞬間のこと」
「この瞬間は、ニューヨーク時間で文字列化すると 『2023-12-01 22:00:00』」
などと言えるわけですよね。
瞬間に名前を付けるとき、名前から瞬間を求めるときに必ず必要になるものがタイムゾーンである、と整理できるわけで、それをその通り型体系に落とし込んだものがBigQuery SQLの日付時刻型だといえます。
この当たり前の型を使っていれば、まずタイムゾーン関連で処理を誤るということがありません。
時刻を記録するときには基本的にTIMESTAMP、それを「その日の0時」「月末の24時」なんていう処理をしたいときにはDATETIMEにいったん変換するのでこのときにどこのタイムゾーンでの話なのかを意識する、という風に正しい処理を強制してくれます。
もちろん、ユーザーに表示したりユーザー入力を解釈するときにもタイムゾーンを使って文字列と相互変換しますからどこのタイムゾーンでのユーザー対話なのか意識することになりますね。当ブログの人気記事のひとつ、タイムゾーンを考慮した日時の扱いのベストプラクティスにもある通りの正しい処理が自然に書けてしまいます。
唯一の不満点を申し上げておきましょう。
これは便利さのためだとは思うのですが、各変換関数でタイムゾーンが省略可能で省略するとデフォルトタイムゾーンが使われるようになっていることです。
省略できてしまうために、「変換するにはタイムゾーンが必要、ゆえにどこのタイムゾーンでの話でしたっけこの要件は?」という意識を強制する働きが中途半端になってしまっていることです*1。
JavaはJava 8より前と以降で日付時刻型体系がガラッと変わりました。
まず、「より前」のJavaについてちょっとだけ言及しておくと、Date
型がBigQueryのTIMESTAMP
型に相当する瞬間型でした。実際にはlong数値をラップしただけの簡単なクラス。
実は瞬間を表す型というのをまず用意していた点は良いセンスでした。しかし、これを日付時刻に変換して処理するために使うCalendar
型のインターフェースが非常に悪く、全体として使いづらい型体系となってしまっていました。
ついでに言うと、瞬間を表すのに「Date」というネーミングも微妙ではありましたよね。
こういった残念さゆえJoda-Timeというサードパーティーライブラリが広く使われていたわけですが*2、そのJoda-Time作者の主導するJSR-310というプロジェクトによりJava 8にDateTime APIと呼ばれる新しい日付時刻型体系が持ち込まれます。これは抜粋すると、以下のような構成の体系です。精度はナノ秒。
絶対時刻型と不定時刻型を備えている、この点ではBigQueryと同じ道具立てが揃っており同様の処理が書けます。
⋯しかし、余計なのです。
瞬間という値にタイムゾーンが必ず一緒に付いてきているのが余計。たとえば「今この瞬間」という瞬間は全地球上で普遍であって東京時間でもソウル時間でもないのに、「東京時間です」もしくは「+09:00です」というラベルも必ず貼らされるのです。
もちろん、必ずこのタイムゾーンというラベルが貼られていることで便利さはあります。絶対時刻を文字列化するなり「月末の24時」みたいな計算をするときにタイムゾーンを用意する必要がなくて便利。
しかし、この便利さは間違っていると私は考えます。
タイムゾーンがどこだか考えないといけない処理を書くときにタイムゾーンを要求されないことは、意図せず間違ったタイムゾーンで処理してしまうリスクにしかならないのだと。
そして、絶対時刻なのに年月日や時分秒も取り出せてしまいますから、そういうのを取り出して処理するにはタイムゾーンを特定して変換しないといけないことに思い至れません。
さて良いニュースですが実はJava 8(DateTime API)にもInstant
型という、タイムゾーンのない絶対時刻型が存在しています。これを使えばBigQueryと同様に正しいプログラミングを強制してもらえるのですが、この型の存在を認知しないままDateTime APIを使っているプログラマも多そうですね。OffsetDateTime、ZonedDateTimeというわかりやすい名前のクラスの存在のせいで多くのプログラマはこちらに引き寄せられてしまい、わざわざこの理解の難しい方を手に取ってしまいます。そして、タイムゾーンよくわからないと悩んでしまいます。
OffsetDateTime、ZonedDateTimeが蛇足だったのです。
C#とはつまり、 .NET Framework の標準ライブラリの日付時刻型ということですね。
一種類の型で表されています。
これは、BigQueryやJavaの型体系を見てきたあとだと一目見てぎょっとしますね。
同じ型が、絶対時刻を表すこともあれば、不定時刻を表すこともあるというのです。「Utc」もしくは「Local」なら絶対時刻、「Unspecified」なら不定時刻。
そして現在時刻を取得するメソッドDateTime.Now
やDateTime.UtcNow
を使うとこの絶対時刻タイプの値が取れるのですが、コンストラクタでDateTime(2023, 12, 2, 12, 0 ,0)
と書くときのデフォルトはUnspecifiedなので不定時刻タイプ。プログラムパス中でも気を付けないと容易に混交してしまいます。型が同じですからね、静的にチェックできません。
さらに恐ろしい事実を。
大小判定(つまり前後判定)するときに、この種類が参照されず、日付時刻部分のみで比較が行われます。
その結果、Utc 2023-12-02 06:00
とLocal(ここでは日本時間) 2023-12-02 09:00
では、後者の方が遅い時刻だと判定されます。わざわざ絶対時刻にしている意味がない。
私は、実用性と美しさを稀に見るハイレベルさで両立させたC#という言語が大好きですが、こと日付時刻型体系に関してだけは改革の余地が大きいという評価です。
Pythonの標準ライブラリdatetimeモジュールの構成は、C#と似ています。
出た、同じ型なのに絶対時刻だったり不定時刻だったりするdatetime型。タイムゾーンを持っていない、不定時刻な場合の値をPython用語ではnaiveと呼び、タイムゾーンを持たせて絶対時刻になっているとawareと呼びますね。
タイムゾーンが「Utc」「Local」の2種類しかないC#に比べれば任意のタイムゾーンが持たせられる分自由度はちょっと高いです。
Pythonのdatetime、C#のDateTimeと同じ欠点があるわけですが、2点においてややましだと言えます。
それはそれとして、Pythonのdatetime型は搭載メソッドが貧弱なのでサードパーティライブラリ(relativedeltaとか、pytzとか)を併用しないと書きたい処理もまともに書けないという困ったところがあり、これはどうにかならないものかといつも思っています。
(Pythonという処理系が環境構築の面で未完成さを抱えており、サードパーティーライブラリを導入すること自体がいろいろなつらみを伴う)
冒頭でBigQueryこそ至高とご紹介しましたが、同じSQL処理系でPostgreSQLも良い型で構成されています。
まったく、BigQueryのTIMESTAMP型、DATETIME型、DATE型、TIME型に対応しています。つまり正しいプログラミングを強制できるはずの体系です。
しかし、二つの点で大きくこの利点を損なってしまっており、この体系の良さを実感できているプログラマはほとんどいません。
timestamp
と書くと不定時刻型になってしまうが、やはりtimestampという言葉は瞬間を指すべきだろう。with time zone
は明らかに間違っている。with
とwithout
の違いがよくわからず、変換の必要性を感じることができない。型体系とはメソッドと一体であることがよくわかる実例となっているのがPostgreSQLなのです。
簡単に言えばこれが日付時刻を扱うための大原則です。
型体系、つまり型の構成とその処理関数・メソッドをあわせたものは、適切に用意すれば良い原則をプログラマに強制することができます。
今回は日付時刻データにフォーカスしてお話いたしましたが、より一般的に、良い扱い方を使い手に強制できるインターフェースというのをモジュール設計においては常に意識したいものですね。
「9時間足せばいいんだっけ、引けばいいんだっけ? ⋯⋯よくわからない、両方試してそれっぽい数字出た方!」で悩むのに比べれば、絶対時刻or不定時刻で考えれば日付時刻処理はよりクリアカットになるの、ご得心いただけましたでしょうか!?
われわれエムスリーのエンジニアは決して、すごい計算力でややこしいことも計算しきって正しいプログラムを書ける超人とかってわけではありません。
そうではなく、シンプルに理解できて間違いにくい組み立てをいつも模索している、そういう人たちです。コード書く前に設計で勝ち、設計する前に概念の組み立てで勝つ、そういう開発環境にちょっとでもご興味ありましたらこちらのページからどうぞ。応募を前提にしないカジュアル面談もやっています。
*1:それゆえ、私はコードレビューの際には省略されているタイムゾーンの明記を促すようにしています。実行環境によってSQL式の意味が意図せず違ってしまうおそれがあるのも困ったことですしね
*2:Joda-Timeのせいで起こってしまった面白事件もあり、それについては18分59秒をめぐって日本標準時の歴史をひもとくことにという記事を書いたことがあります。
*3:ちなみにこのデフォルトタイムゾーンってのは曲者なんですよ、PostgreSQL 9.2 でタイムゾーンのデフォルト値が変わった話 - mallowlabsの備忘録 にあるようにPostgreSQLのマイナーバージョンアップで定義が変わってしまったりします。エムスリーでもこの問題にもろに巻き込まれて大混乱になったことがありました。
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。