Java再入門 ～Java11 日付時刻API

# 過ぎ去りし日々

これまで日付時刻を扱う場合は、以下を利用していました。

* java.util.Date 値を保持する
* java.util.Calendar 値を保持する＆日付の演算をする
* java.text.SimpleDateFormat 日付を指定した書式に整形する。指定した書式の日付からjava.util.Dateを作る
* java.util.TimeZone タイムゾーンを指定する

# 現代

私は全然、日付時刻を真面目に考えたことがなかったので、今回改めて以下の記事を読んで奥深さを学ばせていただきました…

* [タイムゾーン呪いの書 \(知識編\)](https://zenn.dev/dmikurube/articles/curse-of-timezones-common-ja)
* [タイムゾーン呪いの書 \(実装編\)](https://zenn.dev/dmikurube/articles/curse-of-timezones-impl-ja)
* [タイムゾーン呪いの書 \(Java 編\)](https://zenn.dev/dmikurube/articles/curse-of-timezones-java-ja)

## 基礎知識

呪いの書の知識編の自分向けのメモを作ってみたのですが、時間がある人は呪いの書をちゃんと読んでください。
夏時間対応大変だし、海外向けにサービス提供をしないといけない場合もUTC+XX:XXに表現を統一して乗り切りたいと思いましたが…そういうわけにはいかないんですかね

* 「グリニッジ標準時」 (GMT: Greenwich Mean Time)と「協定世界時」 (UTC: Coordinated Universal Time) は世界の時刻の基準となるもの。厳密には違うものだそう…
* オフセットは基準からのズレを使って各国や地域の時刻を表現したもの。UTC+09:00
* 夏時間は日照時間の変化にあわせてその地域の時刻を進ませる制度。夏時間から戻るときは同じ時刻を二度経験することになる
* うるう秒は原子時計の1秒と地球の自転に基づく1秒の差を積みかさねた時のズレを補正するために「59分 60秒」を挿入したり「59分 59秒」を削除したりする考え方
* うるう秒の希釈は「本当は1001秒なんだけど時刻を刻むときには1000秒だったことにしてしまうような考え方」でうるう秒を気にしなくてよいようにする仕組み
* Time Zone Databaseは「Asia/Tokyo」のようなタイムゾーンの名前のこと。略称として「JST」もあるけれど使わない方がいいそう
* UnixタイムはUTC の 1970年 1月 1日 0時 0分 0秒からの経過秒数で表現するもの。うるう秒を無視して同じ秒を二回存在させる。秒以下の値を扱っている時は注意

## java.timeパッケージ

**値を保持する際に使う**
保存する時はOffsetDateTimeを使って、表示する時やユーザーの入力値を処理する時だけZonedDateTimeを使うのがいいのかなと思いました。
LocalDateTimeはこれだけにしか対応していない、自分以外の誰かが作ったものの時に使う必要が出てきます。
現在時刻はInstantなのかもしれませんが、Clockというのがあるので…

* InstantはほぼUnixタイム。うるう秒の処理方法が違うそう
* LocalDateTimeはゾーンやオフセットを持たないどこの時刻だか分からない時刻
* ZonedDateTimeはゾーンは夏時間や制度改変の履歴（日本も過去数回標準時が変わっている）に対応できる反面、その難しさを理解する必要がある
* OffsetDateTimeは世界時からのオフセットを持ったもの

**ゾーンやオフセットの指定をする**

* `ZoneId.of("Asia/Tokyo")`
* `ZoneOffset.of("+09:00")`, `ZoneOffset.ofHours(9)`

**書式を指定する**
従来のjava.text.SimpleDateFormatと違ってこれはスレッドセーフだそうです。

* java.time.format.DateTimeFormatter

**現在時刻を取得する**
Clockは`System.currentTimeMillis()`および`TimeZone.getDefault()`の代わりに使えるよ、だそうです。
現在時刻の取得をする際に、素の`XXX.now()`みたいなのを呼ぶとテストがしにくくなるので、任意の時間にズラすことができるように共通部品を作ることがあると思うのですが、Clockはそういう時に便利そうなClock.fixedというFactory Methodを持っています。

**期間と時間**

経過時間を表現するためのものです。

* Periodは年月日
* Durationは秒とナノ秒
* ChronoUnitは列挙型になっていて、時間量を返してくれる

# 使い方

```java
    @Test
    public void 利用できるゾーンIDを確認する() {
        ZoneId.getAvailableZoneIds().stream().forEach(System.out::println);
    }
```

```java
    @Test
    public void 現在時刻を取得する() {
        {
            ZonedDateTime dateTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
            System.out.println(dateTime);
        }
        {
            OffsetDateTime dateTime = OffsetDateTime.now(ZoneOffset.ofHours(9));
            System.out.println(dateTime);
        }
        {
            Clock clock = Clock.system(ZoneId.of("Asia/Tokyo"));
            ZonedDateTime zonedDateTime = ZonedDateTime.now(clock);
            OffsetDateTime offsetDateTime = OffsetDateTime.now(clock);
            System.out.println(zonedDateTime);
            System.out.println(offsetDateTime);
        }
    }
```
```java
    @Test
    public void 旧世界と新世界() {
        ZonedDateTime dateTime = ZonedDateTime.ofInstant(new Date().toInstant(), ZoneId.of("Asia/Tokyo"));
        Date date = Date.from(dateTime.toInstant());
        System.out.println(date);
    }

```
```java
    @Test
    public void 文字列と日付の相互変換() {
        {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSS");
            System.out.println(formatter.format(ZonedDateTime.now()));
            // 例外が出る。Offsetを持っていない日付は読み取れない
            try {
                OffsetDateTime dateTime = OffsetDateTime.parse("2021/09/23 16:09:02.211", formatter);
            } catch (Exception e) { e.printStackTrace(); }
        }
        {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss.SSSxxxxx");
            OffsetDateTime dateTime = OffsetDateTime.parse("2021/09/23 16:09:02.211+09:00", formatter);
            System.out.println(dateTime);
        }
    }
```
```java
    @Test
    public void 日付と時刻の計算() {
        ZonedDateTime dateTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
        Assertions.assertEquals(dateTime.plus(Duration.ofHours(1)), dateTime.plusHours(1));
        Assertions.assertEquals(Duration.ofHours(1), Duration.between(dateTime, dateTime.plusHours(1)));
        Assertions.assertEquals(1, ChronoUnit.HOURS.between(dateTime, dateTime.plusHours(1)));
    }
```

# シリーズJava再入門

いまからJava8にしよう！だってJava11よりサポート長いし！という判断もあると思いますが、11に引っ越そうとしている方や、11の次のLTSになることを予定されている17への移行を目指している方向けのJava再入門です。

1. [Java7以降の歴史 機能追加とサポート期間](https://qiita.com/yyukawa/items/e1a027bcbfbb3d39958e)
1. [モジュールシステムとクラスパス、Gradle](https://qiita.com/yyukawa/items/44ef24f51649e6c5e299)
1. [Java再入門 JVMオプション](https://qiita.com/yyukawa/items/96f667c7f0c06f97ed3e)
1. [Java再入門 ～Java11 ジェネリクス、ダイアモンド演算子](https://qiita.com/yyukawa/items/d5a5ab8d4846c0b70479)
1. [Java再入門 ～Java11 日付時刻API](https://qiita.com/yyukawa/items/d4159506da46bbd6b651)
1. ～Java11 リソース付きtry文
1. ～Java11 ラムダ式、メソッド参照、Stream、Optional、ローカル変数型推論
1. ～Java11 ExecutorService/Future、Fork/Join
1. 今後：テキストブロック
1. 今後：switch式、パターンマッチングinstanceof
1. 今後：record