JavaのDateTimeFormatterのタイムゾーン部分の扱いメモ

久しぶりにJavaのコードを書いていて、タイムゾーンを含む文字列のパース・整形のルールが分かりにくかったので調べた結果をメモに残します。

## 課題
以下のような日時文字列それぞれパースするとき、また出力したいとき `DateTimeFormatter` として何を使えばよいのか?
-  `ISO_DATE_TIME` `ISO_ZONED_DATE_TIME` `ISO_OFFSET_DATE_TIME` のどれかでよいのか
-  パターンとして `yyyy-MM-dd'T'HH:mm:ssZ` の最後の `Z` の部分を指定するならば何？
```
2023-01-02T04:05:06Z
2023-01-02T04:05:06+0900
2023-01-02T04:05:06+09:00
```

## 結論
- パースするときは、登場する可能性がある形式が以下のどちらかなのかでで判定してパターンを切り替える
  - `+09:00` or `Z` ならば `ZZZZZ` または `ISO_*` を使う(どれでもよい)
    - ただし `+0900` がくると扱いがややこしい
  - `+0900` or `Z` ならば `X` を使う
    - こちらを採用しておき、`+09:00` の対応のために「入力の末尾が `:` ならそれを削除する」という前処理がよさそう
    
- 整形するとき
  - `+0900` と出したい時だけ `Z`、それ以外は `ZZZZZ` または `ISO_*`
    - ただし `Z` を使うと UTCは `Z`ではなく`+0000` になる
    - つまり元の日時のタイムゾーンによって `+0900` と `Z` を出し分けて出力することはできない

## その他

- `ZZZ` や `ZZZZ` を出力につかうと `GMT` が出現したりする (`JST` と出すことはむつかしそう)

## 検証コードと結果

```Main.java
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

public class Main {

    SortedMap<String, DateTimeFormatter> patterns = new TreeMap<>(Map.of(
            "z", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ"),
            "zz", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZZ"),
            "zzz", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZZZ"),
            "zzzz", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZZZZ"),
            "zzzzz", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZZZZZ"),
            "x", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX"),
            "iso", DateTimeFormatter.ISO_DATE_TIME,
            "iso_zoned", DateTimeFormatter.ISO_ZONED_DATE_TIME,
            "iso_offset", DateTimeFormatter.ISO_OFFSET_DATE_TIME));

    List<String> inputs = List.of(
            "2023-01-02T04:05:06",
            "2023-01-02T04:05:06Z",
            "2023-01-02T04:05:06+0900",
            "2023-01-02T04:05:06+09:00");

    String testParse() {

        var result = "";
        for (var i : inputs) {
            result += i + " for\n";
            for (var e : patterns.entrySet()) {
                result += "  " + e.getKey() + " => ";
                try {
                    result += ZonedDateTime.parse(i, e.getValue());
                } catch (Exception ex) {
                    result += ex.toString();
                }
                result += "\n";
            }
            result += "\n";
        }

        return result;
    }

    String testFormat() {
        var utc = ZonedDateTime.parse("2023-01-02T04:05:06+00:00");
        var jst = ZonedDateTime.parse("2023-01-02T04:05:06+09:00");
        var inputs = List.of(utc, jst);

        var result = "";

        for (var i : inputs) {
            result += i + ":\n";
            for (var e : patterns.entrySet()) {
                result += "  " + e.getKey() + " => " + i.format(e.getValue()) + "\n";
            }
        }

        return result;
    }

    public static void main(String[] args) {
        System.out.println(new Main().testParse());
        System.out.println(new Main().testFormat());
    }
}
```

結果
```
2023-01-02T04:05:06 for
  iso => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06' could not be parsed: Unable to obtain ZonedDateTime from TemporalAccessor: {},ISO resolved to 2023-01-02T04:05:06 of type java.time.format.Parsed
  iso_offset => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06' could not be parsed at index 19
  iso_zoned => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06' could not be parsed at index 19
  x => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06' could not be parsed at index 19
  z => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06' could not be parsed at index 19
  zz => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06' could not be parsed at index 19
  zzz => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06' could not be parsed at index 19
  zzzz => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06' could not be parsed at index 19
  zzzzz => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06' could not be parsed at index 19

2023-01-02T04:05:06Z for
  iso => 2023-01-02T04:05:06Z
  iso_offset => 2023-01-02T04:05:06Z
  iso_zoned => 2023-01-02T04:05:06Z
  x => 2023-01-02T04:05:06Z
  z => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06Z' could not be parsed at index 19
  zz => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06Z' could not be parsed at index 19
  zzz => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06Z' could not be parsed at index 19
  zzzz => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06Z' could not be parsed at index 19
  zzzzz => 2023-01-02T04:05:06Z

2023-01-02T04:05:06+0900 for
  iso => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06+0900' could not be parsed, unparsed text found at index 19
  iso_offset => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06+0900' could not be parsed, unparsed text found at index 22
  iso_zoned => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06+0900' could not be parsed, unparsed text found at index 22
  x => 2023-01-02T04:05:06+09:00
  z => 2023-01-02T04:05:06+09:00
  zz => 2023-01-02T04:05:06+09:00
  zzz => 2023-01-02T04:05:06+09:00
  zzzz => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06+0900' could not be parsed at index 19
  zzzzz => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06+0900' could not be parsed at index 19

2023-01-02T04:05:06+09:00 for
  iso => 2023-01-02T04:05:06+09:00
  iso_offset => 2023-01-02T04:05:06+09:00
  iso_zoned => 2023-01-02T04:05:06+09:00
  x => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06+09:00' could not be parsed, unparsed text found at index 22
  z => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06+09:00' could not be parsed at index 19
  zz => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06+09:00' could not be parsed at index 19
  zzz => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06+09:00' could not be parsed at index 19
  zzzz => java.time.format.DateTimeParseException: Text '2023-01-02T04:05:06+09:00' could not be parsed at index 19
  zzzzz => 2023-01-02T04:05:06+09:00


2023-01-02T04:05:06Z:
  iso => 2023-01-02T04:05:06Z
  iso_offset => 2023-01-02T04:05:06Z
  iso_zoned => 2023-01-02T04:05:06Z
  x => 2023-01-02T04:05:06Z
  z => 2023-01-02T04:05:06+0000
  zz => 2023-01-02T04:05:06+0000
  zzz => 2023-01-02T04:05:06+0000
  zzzz => 2023-01-02T04:05:06GMT
  zzzzz => 2023-01-02T04:05:06Z
2023-01-02T04:05:06+09:00:
  iso => 2023-01-02T04:05:06+09:00
  iso_offset => 2023-01-02T04:05:06+09:00
  iso_zoned => 2023-01-02T04:05:06+09:00
  x => 2023-01-02T04:05:06+09
  z => 2023-01-02T04:05:06+0900
  zz => 2023-01-02T04:05:06+0900
  zzz => 2023-01-02T04:05:06+0900
  zzzz => 2023-01-02T04:05:06GMT+09:00
  zzzzz => 2023-01-02T04:05:06+09:00
```