QUO CARD Digital Innovation Lab Tech Blog

クオカード デジタルイノベーションラボの技術ブログです

java.time.LocalDateTime と時差について

こんにちは、日本のテトリスプレイヤーの上位 1% に入る飯島です(デジタルイノベーションラボ所属)。 日付を扱う時によく使用される java.time.LocalDateTime ですが、意外と時差のことを気にせず使っている方も多いのではないのでしょうか。今回は LocalDateTime と時差周りの話、その他 java.time に含まれるクラスの話をしていきます。

LocalDateTime について

LocalDateTime は時差情報を持っていません。 つまり、2020-11-24T12:00:00 を表す LocalDateTime があったとして、それがいったいどの地域に置ける時刻なのかは不明だということです。 日本における 11/24 12:00 はイギリスでは 11/24 3:00 なのに、LocalDateTime ではそれを表すことができません。 日本のシステムと海外のシステムを API 連携するとき等に LocalDateTime でやりとりしていると不整合が出てきてしまいます。 クラウドのリソースを使っていて、アプリケーションサーバーのタイムゾーンと DB サーバーのタイムゾーンが異なっている時も不整合が出てきてしまいます。

不整合が発生する例

弊社では ORM に jOOQ を使うことが多く、jOOQ の java コード生成機能でテーブルに対応するクラスを作って DB を操作するようにしています。 テーブルが timestamp 型のカラムを持つ場合、生成されたコードでは LocalDateTime が対応づけられます。 この場合、アプリケーションサーバーが UTC+9:00, DB サーバーが UTC+0:00 などオフセットが異なっていても、アプリケーションサーバーで作成した日時が DB に保存されます。 例えばアプリケーション側で LocalDateTime.now() をして 2020-12-21 11:44:49.288105 が生成されると、DB にもそのままの形で保存されます。 この値を UTC+0:00 などの別アプリケーションから読み込むと、元々の時差分がずれた日時が取得されてしまいます。

※ timestamp with time zone 型を使うことで時差に対応することは可能です。ただ、自分たちで設計した DB ではない場合など、timestamp 型が前提となっている場面は多々あるかと思います。

じゃあ何を使えば良いのか

基本的には java.time.Instant か java.time.OffsetDateTime を使えば良いかと思います。 LocalDateTime が求められる場面では、LocalDateTime.ofInstant(instant, ZoneOffset.UTC) や LocalDateTime.now(ZoneOffset.UTC) などで 変換するのが良いでしょう。

Instant

時系列上のある一点を表すクラスです。 時差は持っていませんが、LocalDateTime が時系列上の点を表わせないのに対して、こちらは UTC に固定された一点を返します。 試しに手元で現在時刻を表示させてみると、こうなります。

> Instant.now()

2020-11-30T08:35:16.088031Z

画面に Asia/Tokyo の時刻を表示したい場合など、ユーザーが Asia/Tokyo を望んでいる場合は、アプリケーション側で変換して出力すれば良いです。

OffsetDateTime

標準時からの時差情報を含んだデータです。 UTC+8:00 の 13:00 や UTC+9:00 の 15:00 といったデータを持っています。

> OffsetDateTime.now()

2020-11-30T17:35:16.101819+09:00

※ OffsetDateTime#now はシステムのデフォルトタイムゾーンを使用して計算されるため日本の日時が表示されていますが、情報として UTC+9:00 が含まれているため時系列上の一点を表せています。

ZonedDateTime

国・地域ごとの時差情報を持った時刻データです。 1 年を通して常にオフセットが固定の地域もありますが、サマータイムにより 1 年のうちでも標準時を 進める/戻す などオフセットが変わる地域も存在します。 ZonedDateTime ではそういった地域の時差情報を表すことができます。

現在の日本は一年を通してオフセットは一定(UTC+9:00)ですが、実は過去にサマータイムを導入していた時期がありました。 下記コード (kotlin で書いてます) でも確認できます。 1948 年 5 月 2 日の 0:00 ~ 1:00 が存在しないことがわかります。 オフセットも 9 時間から 10 時間になっていますね。

val zonedDateTime = ZonedDateTime.of(1948, 5, 1, 20, 0, 0, 0, ZoneId.of("Asia/Tokyo"))
(1..10).fold(zonedDateTime) { dt, i ->
  dt.plusHours(1).also { println(it) }
}

1948-05-01T21:00+09:00[Asia/Tokyo]
1948-05-01T22:00+09:00[Asia/Tokyo]
1948-05-01T23:00+09:00[Asia/Tokyo]
1948-05-02T01:00+10:00[Asia/Tokyo]
1948-05-02T02:00+10:00[Asia/Tokyo]
1948-05-02T03:00+10:00[Asia/Tokyo]
1948-05-02T04:00+10:00[Asia/Tokyo]
1948-05-02T05:00+10:00[Asia/Tokyo]
1948-05-02T06:00+10:00[Asia/Tokyo]
1948-05-02T07:00+10:00[Asia/Tokyo]

国・地域ごとの時差情報は、デフォルトでは tz database から取得するようになっています。


以上、java.time.LocalDateTimeは時差情報を持っていないので使うときは気をつけましょうという話でした。 それでは楽しい時差ライフを!!