[初心者向け] 日付の取り扱いでビックリした話

# はじめに
* 本来はあまり発生するような話ではないので、読み物としてお楽しみいただければと思います。

# ある日の会社でのできごと
後輩　「UTCからJSTに変換する際に、冬場に該当する月（12月～3月）だと正しく変換できない。JavaのDate().getTimezoneOffset()を使っている。なんでDateだとうまくいかないのかは不明！」

我　「(今時、java.util.Date使ってることなんてあるのかな…いにしえのコードなのか？はたまた海の外から納品されたレアなパターン踏んじゃったのかな...) 理由があることをきちんと説明しないといけない！（謎の使命感）」



# 日付の取り扱い（歴史）
## Java8以前の話
![日付を取り巻くクラス.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180558/67c72835-43c6-68f9-523f-6100d12d310d.png)


* 日付を表すのに、`java.util.Date` 、計算するために、`java.util.Calendar` を使っていました。
    - `java.util.Date`
        - `Java 日付` でググると先頭に出てくる代表的なクラス。特定のタイミングの日時を表現しているクラス。
            - これがウワサの [一番最初に出てくるJava8のDateクラスのjavadoc](https://docs.oracle.com/javase/jp/8/docs/api/java/util/Date.html)
                - JDK1.0でリリースされ、大半のコンストラクタやメソッドがJDK1.1で非推奨になってしまったという悲しい歴史を持つクラスです。(会話の中に出てきた、`getTimeZoneOffset()`も含まれます。)
                    - こちらの記事がとても勉強になります。
                        -  [素人にはお薦め出来ない「非推奨」 - JDK1.0のお話](https://qiita.com/e99h2121/items/9ab17b38ecf6d4950f4d)  

    - `java.sql.`パッケージ
        - PreparedStatementで利用。
        - あと、ORマッパー等でも利用。
            - 参考：[Doma 2.8のドキュメント](https://doma.readthedocs.io/en/2.8.0/basic/)
        - `java.sql.Date`
            - 日付を取り扱いたい場合に利用。
        - `java.sql.Time`
            - 時刻を取り扱いたい場合に利用。
        - `java.sql.Timestamp`
            - 日時を取り扱いたい場合に利用。      
    - `java.util.Calendar`
        - 日付の演算、取得、比較等、日付の操作で利用する。 
    - `java.text.SimpleDateFormat`
        - 日付のフォーマットと解析を行う。 

> JDK1.0は1996年、JDK1.1は1997年にリリースされています。
> 改善されるきっかけになるJava8は2014年…


## Java8以降の話
![日付を取り巻くクラス_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180558/3a42301e-b852-645d-7c09-af8ff38e2364.png)


* [JSR 310: Date and Time API](https://jcp.org/en/jsr/detail?id=310) がリリースされ、`新しいAPIの最終的な目標は、シンプルに使えることである。`　とあるように、日付の取り扱いがずいぶん楽になりました。 
    - 楽になったポイントの一例
        - 年月日を指定してインスタンスを作るのが楽になった。
        - Calendarクラスの月は0始まり（ここ、一度は誰もがハマったはず）だったのが、1始まりになった。
            - 配列利用での利便性を考え、0始まり（月そのものではなく、インデックス）だった。 
        - 日付の計算のためにCalendarクラスを使う必要がなくなった。

    - `java.time.LocalDate`
        - 日付を表すクラス。 (年、月、日 (yyyy-MM-dd))
    - `java.time.LocalTime`
        - 時間を表すクラス。 (時、分、秒、ナノ秒 (HH-mm-ss-ns))
    - `java.time.LocalDateTime`
        - 日付と時刻の両方を表すクラス。 (yyyy-MM-dd-HH-mm-ss-ns)　※タイムゾーンなし
    - `java.time.ZonedDateTime`
        - 日付と時刻の両方を表すクラス。 (yyyy-MM-dd-HH-mm-ss-ns)　※タイムゾーンあり


## （説明だけだと分かりづらいので）実際のコード
### システム日付取得
* Java8以前
    ```java
    // 引数なしのコンストラクタは非推奨にはなっていない
    Date systemDate = new Date();
    // このまま出力しちゃうと、日付以外の情報も出ちゃう　ex> Sun Dec 06 22:18:35 JST 2023
    System.out.println(systemDate);
    // yyyy-MM-dd等、フォーマットした状態で出したい場合はひと手間必要。
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
     // これでようやく2023-12-06が出力される
    System.out.println(sdf.format(systemDate);
    ```

* Java8以降
    ```java
    // これだけ！行数半分！
    LocalDate systemDate = LocalDate.now();
    System.out.println(systemDate);
    ```

### 指定した日付でのインスタンス生成
* Java8以前
    ```java
    // Dateクラスにも年月日指定をできるコンストラクタは存在しますが、非推奨なのでCalendarを利用する。
    Calendar calendar = Calendar.getInstance();
    calendar.set(Calendar.YEAR, 2023);
    // 月は0始まりなので、1引かないといけない
    calendar.set(Calendar.MONTH, 11);
    calendar.set(Calendar.DAY_OF_MONTH, 6);
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    System.out.println(sdf.format(calendar.getTime()));
    ```
    
* Java8以降
    ```java
    // 12月が12なのがありがたい…
    LocalDate targetDate = LocalDate.of(2023, 12, 6); 
    System.out.println(targetDate); 
    ```

### 日付の加算・減算
* Java8以前
    ```java
    Date now = new Date();
    Calendar calendar = Calendar.getInstance();
    calendar.setTime(now);
    // 1か月前
    calendar.add(Calendar.MONTH, -1);
    // 1日後
    calendar.add(Calendar.Date, 1);
    ```

* Java8以降
    ```java
    LocalDate now = LocalDate.now();
    // 1か月前
    now.minusMonths(1);
    // 1日後
    now.plusDays(1);
    ```

### 文字列から日付を生成
* Java8以前
    ```java
    // parseメソッドはParseExceptionをthrowするので例外処理の対処が必要です。
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
    Date date = sdf.parse("2023/12/06");
    System.out.println(sdf.format(date));
    ```
    > SimpleDateFormatはスレッドセーフでないので、利用する際は注意が必要。（意図しないところで値が書き換えられる可能性があります。）
    
* Java8以降
    ```java
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd");
    LocalDate targetDate = LocalDate.parse("2023/12/0", formatter);
    System.out.println(targetDate);
    ```
    > DateTimeFormatterはスレッドセーフです。 





## 後輩とのその後
java.util.Dateの非推奨メソッドである旨と、java.text.ZonedDateTimeの利用で解消する旨を簡単に説明し、下記のソースコードを提示しました。

``` java
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
​
​
public class ZoneDateTimeTest {
	public static void main(String[] args) {
		// 元のUTCの時刻文字列
		String utcTime = "2023-09-30T15:00:00.000+0000";
		
		// ZonedDateTimeでparseできるフォーマット（この形じゃないとできない）★今回、ここでちょっとハマったのはここだけの話。
		String dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS+SSSS";
		
		// パース出来るフォーマットでUTCの時刻をJSTの時刻に変換
		ZonedDateTime utc = ZonedDateTime.parse(time, DateTimeFormatter.ofPattern(dateFormat).withZone(ZoneId.of("UTC")));
		ZonedDateTime jst = utc.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
		
		System.out.println("utc=" + utc);
		System.out.println("jst=" + jst);
		
		// 標準出力の結果は下記になる
		// utc=2023-09-30T15:00Z[UTC]
		// jst=2023-10-01T00:00+09:00[Asia/Tokyo]
	}
}
```


帰ってきた返信は…
「色々と確認ありがとうございます！実はJavaではなくjavascriptでした。Javaと似ていて混乱してしまいました。」

> 問題のjavascriptですが某SaaSサービスが提供しているものでしたが、確かにメソッド名とか、Javaっぽかったですが…


～しばしの沈黙～

あれ、Javaの話じゃなかったのかよ…（なんか私滑ってない？
なんか滑って終わる感じがなんともですが、歴史を再認識できてよかったことにしたいです。


[2023/12/06 18:49 追記]
※今回はこのソースコードを使う場面が出てこなかったので事なきを得ましたが、[java.timeパッケージのjavadoc](https://docs.oracle.com/javase/jp/17/docs/api/java.base/java/time/package-summary.html#:~:text=%E5%8F%AF%E8%83%BD%E3%81%A7%E3%81%82%E3%82%8C%E3%81%B0%E3%80%81%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%BE%E3%83%BC%E3%83%B3%E3%81%8C%E3%81%AA%E3%81%84%E5%8D%98%E7%B4%94%E3%81%AA%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%82%92%E3%81%8A%E8%96%A6%E3%82%81%E3%81%97%E3%81%BE%E3%81%99%E3%80%82%20%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%BE%E3%83%BC%E3%83%B3%E3%82%92%E5%BA%83%E7%AF%84%E3%81%AB%E4%BD%BF%E7%94%A8%E3%81%99%E3%82%8B%E3%81%A8%E3%80%81%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%8C%E3%81%8B%E3%81%AA%E3%82%8A%E8%A4%87%E9%9B%91%E3%81%AB%E3%81%AA%E3%82%8B%E5%82%BE%E5%90%91%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82)　に「可能であれば、タイムゾーンがない単純なクラスを使用することをお薦めします。 タイムゾーンを広範に使用すると、アプリケーションがかなり複雑になる傾向があります。」とあるので、安易に使う事はおすすめしません。(@skrb さん、ありがとうございました。）
安易に使うと呪われます。

呪われる件については、下記がとても勉強になりました。
[タイムゾーン呪いの書 (知識編)](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)

## まとめ
* 日付の操作は、`Date and Time API` を使いましょう。
* なんかおかしいな、と思ったら非推奨かどうかを確認しましょう。（今回は、そもそも…の話ではありましたが。）


## おまけ（日付の話で思い出したことがあったので。）
昔(おそらくJava5くらいのころ）に、`java.util.Date` のcompareToメソッドを使って、`java.sql.Timestamp` と比較したところ、Timestampの秒以下が無視されて、意図しない結果になったことがあったなと思い出して、試してみたところ。20で実行すると思った結果が返ってきてびっくりしました。

```java
System.out.println(new java.util.Date(10000).before(new java.sql.Timestamp(20000)));
System.out.println(new java.util.Date(10000).before(new java.sql.Timestamp(10999)));
```

これも8で動きが変わったのかと思いつつ、いつ時点で治ったのか…(1.8.0_211時点では治っていない…)
[2023/12/06 17:24 追記] 9で修正が入ったようです。@ktgw0316 さん、ありがとうございます。

* 1.5.0
![java_5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180558/308ad376-cd9d-7cc0-ecfb-c72d9f11df06.jpeg)
> windows + r、cmdでプロンプトでないんですね…(commandでした)

* 1.8.0_211
![java_1_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180558/8048647c-be37-52bd-101c-b798bdda6602.png)


* 20
![java_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180558/2fe9c38c-b062-58e9-0bbf-6d0341ac49c5.png)
> JShellはやっぱり便利ですね…