はじめに
タイムゾーンがある場合に迷ったりしたので、LocalDateTimeを使用した日付の処理についてまとめました。
目次
1. now()で日時を取得する
2. format(LocalDateTimeから文字列への変換)
3. parse(文字列からLocalDateTimeに変換)
4. LocalDateTime同士の比較について
5. UNIX時間(エポック秒)の変換
6. Use case
公式ドキュメント
ZonedDateTime (Java Platform SE 8 )
DateTimeFormatter (Java SE 17 & JDK 17)
ZoneId (Java Platform SE 8 )
1. now()で日時を取得する
// タイムゾーンなしのUTC、JSTそれぞれで取得したい場合
LocalDateTime.now(ZoneId.of("UTC"));
LocalDateTime.now(ZoneId.of("Asia/Tokyo"));
LocalDateTime.now(); // 実行されるサーバによってタイムゾーンが異なるため注意
// 2021-08-30T09:00:00.123456
// 2021-08-30T18:00:00.123456
// 2021-08-30T18:00:00.123456 <- ローカルで実行した場合はJSTでした
// タイムゾーンありの日付を取得したい場合(戻り値は ZonedDateTime クラスとなる)
LocalDateTime.now(ZoneId.of("UTC")).atZone(ZoneId.of("UTC"));
LocalDateTime.now(ZoneId.of("UTC")).atZone(ZoneId.of("Asia/Tokyo"));
LocalDateTime.now(ZoneId.of("Asia/Tokyo")).atZone(ZoneId.of("UTC"));
LocalDateTime.now(ZoneId.of("Asia/Tokyo")).atZone(ZoneId.of("Asia/Tokyo"));
// 2021-08-30T09:00:00.123456Z[UTC] <- OK
// 2021-08-30T09:00:00.123456+09:00[Asia/Tokyo] <- ※NG
// 2021-08-30T18:00:00.123456Z[UTC] <- ※NG
// 2021-08-30T18:00:00.123456+09:00[Asia/Tokyo] <- OK
// ※NGパターン (時間がずれます)
LocalDateTime.now(ZoneId.of("UTC")).atZone(ZoneId.of("Asia/Tokyo"));
LocalDateTime.now(ZoneId.of("Asia/Tokyo")).atZone(ZoneId.of("UTC"));
// 2021-08-30T09:00:00.123456+09:00[Asia/Tokyo]
// 2021-08-30T18:00:00.123456Z[UTC]
// 初めからZonedDateTimeで取得することもできます
// タイムゾーンありの場合はこちらで取得した方が良さそうです
ZonedDateTime.now(ZoneId.of("UTC"));
ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
// 2021-08-30T09:00:00.123456Z[UTC]
// 2021-08-30T18:00:00.123456+09:00[Asia/Tokyo]
【要点】
タイムゾーンのあり/なしに関わらず、now()の引数に指定したZoneIdの日時が取得されます。
2. format(LocalDateTimeから文字列への変換)
// タイムゾーンなし
LocalDateTime localDateTime = LocalDateTime.now(ZoneId.of("UTC"));
localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
localDateTime.format(DateTimeFormatter.ISO_LOCAL_DATE);
// 2021-08-30T09:00:00.123456
// 2021-08-30
// タイムゾーンあり
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("UTC"));
zonedDateTime.format(DateTimeFormatter.ISO_DATE_TIME);
zonedDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSSSSxxx"));
// 2021-08-30T09:00:00.123456Z[UTC]
// 2021-08-30T09:00:00.123456+00:00
// フォーマットの際にJSTに変換
zonedDateTime.format(DateTimeFormatter.ISO_DATE_TIME);
zonedDateTime.format(DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneId.of("Asia/Tokyo")));
zonedDateTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo")).format(DateTimeFormatter.ISO_DATE_TIME);
// 2021-08-30T09:00:00.123456Z[UTC]
// 2021-08-30T18:00:00.123456+09:00[Asia/Tokyo]
// 2021-08-30T18:00:00.123456+09:00[Asia/Tokyo]
【要点】
以下のformatterが主に使用されると思うので対応を覚えておくといいと思います。
形式 | タイムゾーン | formatter |
---|---|---|
年月日 + 時分秒 | なし | ISO_LOCAL_DATE_TIME |
年月日 | なし | ISO_LOCAL_DATE |
年月日 + 時分秒 | あり | ISO_DATE_TIME |
3. parse(文字列からLocalDateTimeに変換)
// タイムゾーンなし
LocalDateTime.parse("2021-08-30T09:00:00.123456", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
// 年月日をフォーマットする場合はLocalDateクラスになっていることに注意
LocalDate.parse("2021-08-30", DateTimeFormatter.ISO_LOCAL_DATE);
// 2021-08-30T09:00:00.123456
// 2021-08-30
// タイムゾーンあり
ZonedDateTime.parse("2021-08-30T09:00:00.123456Z", DateTimeFormatter.ISO_DATE_TIME);
DateTimeFormatter.ISO_DATE_TIME.parse("2021-08-30T09:00:00.123456Z", ZonedDateTime::from);
// 2021-08-30T09:00:00.123456Z
// 2021-08-30T09:00:00.123456Z
// LocalDateTimeでもフォーマットできますが、タイムゾーンがなくなるのでお勧めしません
// タイムゾーンありの文字列はformatterをISO_DATE_TIMEにしないとエラーとなります
LocalDateTime.parse("2021-08-30T09:00:00.123456Z", DateTimeFormatter.ISO_DATE_TIME);
LocalDateTime.parse("2021-08-30T18:00:00.123456+09:00", DateTimeFormatter.ISO_DATE_TIME);
// 2021-08-30T09:00:00.123456
// 2021-08-30T18:00:00.123456
// フォーマット時にJSTに変換することもできます
ZonedDateTime.parse("2021-08-30T09:00:00.123456Z", DateTimeFormatter.ISO_DATE_TIME.withZone(ZoneId.of("Asia/Tokyo")));
// 2021-08-30T18:00:00.123456+09:00[Asia/Tokyo]
【要点】
タイムゾーンありの文字列は基本的にZonedDateTimeを使用した方が良いと思います。
後述しますが、ZonedDateTime同士で比較することでタイムゾーンを意識しなくて良くなるためです。
4. LocalDateTime同士の比較について
// equalsではなくisEqualを使用するのに注意
ZonedDateTime utc = ZonedDateTime.parse("2021-08-30T09:00:00.123456+00:00", DateTimeFormatter.ISO_DATE_TIME);
ZonedDateTime jst = ZonedDateTime.parse("2021-08-30T18:00:00.123456+09:00", DateTimeFormatter.ISO_DATE_TIME);
utc.isEqual(jst);
// true
ZonedDateTime utc = ZonedDateTime.parse("2021-08-30T09:00:01.123456+00:00", DateTimeFormatter.ISO_DATE_TIME);
ZonedDateTime jst = ZonedDateTime.parse("2021-08-30T18:00:00.123456+09:00", DateTimeFormatter.ISO_DATE_TIME);
utc.isAfter(jst);
utc.isBefore(jst);
// true
// false
【要点】
ZonedDateTimeで比較することでUTCとJSTでタイムゾーンが異なっても正しく比較できます。
isAfter
は引数より自分が未来か、isBefore
は引数より自分が過去かという風に、引数の日付を起点に過去か未来かを考えると理解しやすいと思います。
5. UNIX時間(エポック秒)の変換
// LocalDateTime -> エポック秒
LocalDateTime localDateTimeUtc = LocalDateTime.parse("2021-08-30T09:00:00.123456", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
LocalDateTime localDateTimeJst = LocalDateTime.parse("2021-08-30T18:00:00.123456", DateTimeFormatter.ISO_LOCAL_DATE_TIME);
localDateTimeUtc.toEpochSecond(ZoneOffset.UTC);
localDateTimeUtc.toEpochSecond(ZoneOffset.ofHours(9)); // NG
localDateTimeJst.toEpochSecond(ZoneOffset.UTC); // NG
localDateTimeJst.toEpochSecond(ZoneOffset.ofHours(9));
// 1630314000 -> 2021-08-30T09:00Z
// 1630281600 -> 2021-08-30T00:00Z
// 1630346400 -> 2021-08-30T18:00Z
// 1630314000 -> 2021-08-30T09:00Z
// ZonedDateTimeの場合
ZonedDateTime zonedDateTimeUtc = ZonedDateTime.parse("2021-08-30T09:00:00.123456Z", DateTimeFormatter.ISO_DATE_TIME);
ZonedDateTime zonedDateTimeJst = ZonedDateTime.parse("2021-08-30T18:00:00.123456+09:00", DateTimeFormatter.ISO_DATE_TIME);
zonedDateTimeUtc.toEpochSecond();
zonedDateTimeJst.toEpochSecond();
// 1630314000 -> 2021-08-30T09:00Z
// 1630314000 -> 2021-08-30T09:00Z
// エポック秒 -> LocalDateTime
// UTC : 2021-08-30T09:00Z
Instant instant = Instant.ofEpochSecond(1630314000);
// ZonedDateTimeに変換
instant.atZone(ZoneId.of("UTC"));
instant.atZone(ZoneId.of("Asia/Tokyo"));
// 2021-08-30T09:00Z[UTC]
// 2021-08-30T18:00+09:00[Asia/Tokyo]
// LocalDateTimeに変換
instant.atZone(ZoneId.of("UTC")).toLocalDateTime();
LocalDateTime.ofInstant(instant, ZoneId.of("UTC"));
// 2021-08-30T09:00
// 2021-08-30T09:00
【要点】
LocalDateTimeからエポック秒に変換する際は、タイムゾーンがどこなのかを意識していないと時間がずれてしまいます。
ZonedDateTimeで保持しておけばタイムゾーンを意識せずエポック秒に変換できるので便利です。
6. Use case
// JSTの文字列をUTC文字列に変換
LocalDateTime
.parse("2021-08-30T18:00:00", DateTimeFormatter.ISO_LOCAL_DATE_TIME)
.atZone(ZoneId.of("Asia/Tokyo"))
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.of("UTC")))
// 文字列で日時(UTC)を受け取り、タイムゾーンなしの文字列(JST)に変換
ZonedDateTime
.parse("2021-08-30T09:00:00.123456Z", DateTimeFormatter.ISO_DATE_TIME)
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME.withZone(ZoneId.of("Asia/Tokyo")));
// 2021-08-30T18:00:00.123456
// now()で現在日付を取得した後、前日の23時59分59秒を設定する
LocalDateTime.now(ZoneId.of("Asia/Tokyo"))
.minusDays(1)
.toLocalDate()
.atTime(LocalTime.MAX)
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
// 2021-08-29T23:59:59.999999999
LocalTime.MAX
は秒以下が9桁になるので、用途によっては独自にフォーマットした方が良いかもしれません。
おわりに
この記事を書いている中で思ったのは、ZonedDateTimeで初めにタイムゾーンを決めて保持しておけばそれ以降変換の際にタイムゾーンを意識しなくてもよいので基本的にはZonedDateTimeで保持した方が良いのではないかということです。
LocalDateTimeだとZonedDateTimeやエポック秒に変換する際にタイムゾーンがUTCかJSTのどっちだったか気にする必要があります。
もしくは常にタイムゾーンはUTCに統一するなど、工夫して管理する必要があると思います。
初歩的ですが、タイムゾーンがずれて不具合が発生することはどこの現場でもまあまあ発生します。