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