ZonedDateTime覚書
私はアプリ内で時刻を取り扱う際には、基本的にZonedDateTimeを使うようにしている。しているのだが、基本のセットアップをした後はあまり複雑なことをしない。そのため別プロジェクトで新たにセットアップをする際、いつも「あれ、どうやるんだっけ」となってしまう。
一応個人的メモは持っているものの非効率なので、よく使う設定を覚書としてメモしておこうと思う。ちなみにAndroidで使う場合の話をメインにするが、JavaでZonedDateTimeを扱うときと基本は同じだと思う。
正直なところ理解があやふやな部分もあるので、間違いがあったら指摘してほしい。
まずはセットアップ。AndroidでZonedDateTimeを使おうと思ったら、ThreeTenABPを使うことになるだろう。
build.gradleに依存を追加して・・・というあたりは割愛する。
ちなみにカスタムApplicationクラスにおいてAndroitThreeTen.init(this)
で初期化するのをよく忘れる。Zone DBが初期化されていないというエラーとともにアプリがクラッシュするので気をつけよう1。
Robolectricを使ったユニットテストをするのであればたぶん問題ない話かもしれないが、JVMのユニットテストでZonedDateTimeを使ってテストを書く場合には一工夫が必要である。
ThreeTenABPは、TimezoneのDBをApplicationクラスで初期化してはじめて使えるようになる。ということは、Applicationクラスを経由しないユニットテストではTimezoneのDBが存在しないことになってしまう。
回避策はテストの際はthreetenbpを使うというだけだ。testImplementation "org.threeten:threetenbp:1.3.1"
(バージョンは適宜調整されたし)と、テストでは本家の依存を追加することで回避できる。
まあこのissueを見て私はそうしているというだけである。
ZonedDateTimeをアプリケーション内で扱うとして、DBにはそのままでは保存できない。何らかの形で変換が必要だ。
変換先をどうするかは人それぞれだろうが、私はLong
を選ぶことが多い。で、困ったことにこのLong
への変換・復元操作が一筋縄ではいかないので毎回困るのである。(忘れているという意味で)
まず大前提として、アプリケーション内で扱う時刻はすべてUTCに変換して扱う。Longへ変換は、つまるところepochSecondへの変換なのだが、ここではTimezone情報が抜け落ちる。だからLongへ変換する際には必ずタイムゾーンをUTCに変換してから処理を行う。
私は毎回こんな感じで拡張関数として定義しておく。
fun ZonedDateTime.toUtcLongMillis(): Long {
val utcZDT = this.withZoneSameInstant(ZoneOffset.UTC)
.truncatedTo(ChronoUnit.MILLIS)
return utcZDT.toInstant().toEpochMilli()
}
fun Long.toZonedDateTimeUtc(): ZonedDateTime {
val instant = Instant.ofEpochMilli(this)
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)
}
注意することというか、補足することは次のようなこと。
withZoneSameInstant(ZoneOffset.UTC)
でUTCでの時刻に変換するtruncatedTo()
でナノ秒以下を切り捨てているのは別にいらないかも2- 一度
Instant
に変換してからtoEpochMilli()
でミリ秒(Long)に変換する - 復元する際は
ofEpochMilli
でLongからInstantに変換した後、そのInstantからZonedDateTimeを復元する
Instantは私の理解では、刻々と変化する時間のとある一時点を示す情報だ。しかしその「とある一時点」は、具体的に何年何月何日の何時何分何秒なのかは不明である。その情報はタイムゾーンが指定されることではじめて分かるからだ。
時間には時差があるので、時差の情報はタイムゾーンが指定されなければ分からない。時差が分からなければ、「とある一時点」だけ伝えられても、時差次第でどうとでも変化してしまうからだ。同じInstantでもUTCでは2019年6月17日の9時8分5秒であり、日本(JST)では2019年6月17日の18時8分5秒だからだ。
そのためZonedDateTimeは時間の一時点を表すInstantと、タイムゾーンに関する情報の2つを持つようになっている。
そもそも時間の概念がややこしい。ZonedDateTimeを扱うようになってから余計にそう思うようになった。