LocalDateTimeの相互変換 決定版

# はじめに
タイムゾーンがある場合に迷ったりしたので、LocalDateTimeを使用した日付の処理についてまとめました。

# 目次
[1. now()で日時を取得する](#1-nowで日時を取得する)
[2. format（LocalDateTimeから文字列への変換）](#2-formatlocaldatetimeから文字列への変換)
[3. parse（文字列からLocalDateTimeに変換）](#3-parse文字列からlocaldatetimeに変換)
[4. LocalDateTime同士の比較について](#4-localdatetime同士の比較について)
[5. UNIX時間（エポック秒）の変換](#5-unix時間エポック秒の変換)
[6. Use case](#6-use-case)

## 公式ドキュメント
[ZonedDateTime (Java Platform SE 8 )](https://docs.oracle.com/javase/jp/8/docs/api/java/time/ZonedDateTime.html)
[DateTimeFormatter (Java SE 17 & JDK 17)](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/format/DateTimeFormatter.html)
[ZoneId (Java Platform SE 8 )](https://docs.oracle.com/javase/jp/8/docs/api/java/time/ZoneId.html)


## 1. now()で日時を取得する
```java
// タイムゾーンなしの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]
```
<font color="LightCoral">**【要点】**</font>
タイムゾーンのあり/なしに関わらず、now()の引数に指定したZoneIdの日時が取得されます。


## 2. format（LocalDateTimeから文字列への変換）
```java
//　タイムゾーンなし
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]
```
<font color="LightCoral">**【要点】**</font>
以下のformatterが主に使用されると思うので対応を覚えておくといいと思います。

|形式|タイムゾーン|formatter|
|:--|:--|:--|
|年月日 + 時分秒|なし| ISO_LOCAL_DATE_TIME |
|年月日|なし|ISO_LOCAL_DATE|
|年月日 + 時分秒|あり|ISO_DATE_TIME|


## 3. parse（文字列からLocalDateTimeに変換）
```java
//　タイムゾーンなし
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]
```
<font color="LightCoral">**【要点】**</font>
タイムゾーンありの文字列は基本的にZonedDateTimeを使用した方が良いと思います。
後述しますが、ZonedDateTime同士で比較することでタイムゾーンを意識しなくて良くなるためです。


## 4. LocalDateTime同士の比較について
```java
// 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
```
<font color="LightCoral">**【要点】**</font>
ZonedDateTimeで比較することでUTCとJSTでタイムゾーンが異なっても正しく比較できます。
`isAfter`は引数より自分が未来か、`isBefore`は引数より自分が過去かという風に、引数の日付を起点に過去か未来かを考えると理解しやすいと思います。


## 5. UNIX時間（エポック秒）の変換
```java
// 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
```
<font color="LightCoral">**【要点】**</font>
LocalDateTimeからエポック秒に変換する際は、タイムゾーンがどこなのかを意識していないと時間がずれてしまいます。
ZonedDateTimeで保持しておけばタイムゾーンを意識せずエポック秒に変換できるので便利です。


## 6. Use case
```java
// 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に統一するなど、工夫して管理する必要があると思います。
初歩的ですが、タイムゾーンがずれて不具合が発生することはどこの現場でもまあまあ発生します。