JavaのSimpleDateFormatでparse時に実在日時チェック

#存在しない日付がparseされる
最小限のコーディングでのサンプル。

```java:normal
String target = "2019/07/20 12:34:56";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
try{
    Date result = sdf.parse(target);
    System.out.println(sdf.format(result));
}catch(ParseException e){
    e.printStackTrace();
}
```
```console
2019/07/20 12:34:56
```
入力した`String`の値通りにparseとformatが実行される。

ここで、`String`の値を`2020/27/72 72:72:72`に設定したときの動きを検証する。
大体パースするような処理では、この値のように、実在しない日時、
カレンダーや時計で表示され得ない日時のデータは、
異常入力として、パースを失敗させたい、はずだ。
この記事はそういう要件向けに書いている。

```java:abnormal
String target = "2020/27/72 72:72:72";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
try {
    Date result = sdf.parse(target);
    System.out.println(sdf.format(result));
} catch (ParseException e) {
    e.printStackTrace();
}
```
```console
2022/05/14 01:13:12
```
動作で言えば、`ParseException`が吐かれることなく、それっぽく変換が成されるのだ。

#そんなデータをparseさせたくない
1ステップの書き足しだけで、実在する日時かの検証までを実行可能である。

```java:1ステップ追加
String target = "2020/27/72 72:72:72";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
/* setLinient(boolean) の呼び出しを追加 */
sdf.setLenient(false);
try {
    Date result = sdf.parse(target);
    System.out.println(sdf.format(result));
} catch (ParseException e) {
    e.printStackTrace();
}
```
```console
java.text.ParseException: Unparseable date: "2020/27/72 72:72:72"
	at java.text.DateFormat.parse(DateFormat.java:366)
	at ...
```
`ParseException`を吐いてくれるので、
他の入力値不正のケース（この実装例で言えば、`"令和元年七月二十日"` とか）と同様に、
例外を拾うことで対処してあげることができる。
どう対処するのかは、実装の要件次第なので、特に触れず。

コーディング上の対応方法を知りたい方はここまでで終わり。
この後は、呼び出したメソッドの背景をちょっと探索してみた調査録。

---
#2020/27/72 72:72:72 = 2022/05/14 01:13:12？
先ほどのコード例で挙げたデータがなんでこの日時になるのか、の軽い解説。
日時の内、時間部分だけをさらっと確認。
72時 = 24時間x3
72分 = 60分x1 + 12分
72秒 = 60秒x1 + 12秒
→ (12秒 + 1分) + (12分 + 1時間) + 3日
→ 12秒 + 13分 + 1時間
同様に、日付部分にまで3日分の繰り込みと、各月とかからあふれた分の算入が行われている。

#Java仕様書での記載確認
[Java8 API(SimpleDateFormat)](https://docs.oracle.com/javase/jp/8/docs/api/java/text/SimpleDateFormat.html)での記載を検証する。
##入力値の桁数のチェック仕様
フォーマットとして、`"yyyy/MM/dd HH:mm:ss"`を指定しているのだから、
MMは2桁、ddも2桁... じゃないとNGになるのでは？というのが幻想のようなので、確認。

```java:input
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
sdf.setLenient(false);
String zeropad = "2019/01/02 03:04:05";  //1桁の箇所も2桁になるよう0埋めしている
String lessdig = "2019/1/2 3:4:5";   //1桁の時の0埋めなし
try{
    sdf.parse(zeropad);
    sdf.parse(lessdig);
...
```
```console
   （ParseExceptionは発生しない）
```
この挙動については、APIで記載されている。
>数値: フォーマット時に、パターン文字の数は最小桁数です。これより短い数値は、この桁数までゼロ埋めされます。解析には、2つの隣接するフィールドを区切る必要がないかぎり、パターン文字の数は無視されます。

**文字の数は無視**されるのだ。

※yyyyとyy、 MMとMMM など、そもそも「数値」に該当するかどうかが変化するものについては、注意が必要だが、
数値として解釈されるフィールドについては、パターン文字の数は、解釈に影響しない。

##入力値の実在日時チェック仕様
`parse(String)`のデフォルトでの日付チェック（API上では「厳密な解析」といった表現）については、
SimpleDateFormat のスーパークラスである、 [DateFormatのAPI](https://docs.oracle.com/javase/jp/8/docs/api/java/text/DateFormat.html)に記載があった。

>デフォルト値では、解析は厳密ではありません。入力が、このオブジェクトのフォーマット・メソッドで使用される形式ではないが、日付として解析可能であれば、解析は正常に行われます。

SimpleDateFormat.parse(String) → DateFormat.parse(String) → DateFormat.parse(String, ParsePosition)
という流れで記載に行きつくことができた。

このAPIの記載、上の一文に続けて、このように書かれていた。

>クライアントは、setLenient(false)を呼び出すことによって、このフォーマットを厳密に要求できます。

なるほど、APIって最強のドキュメントだね。

##setLenient(false)とは何者
結論から言えば、`Calendar`クラスのメソッドである。
[Calendar.setLinient(boolean)](https://docs.oracle.com/javase/jp/8/docs/api/java/util/Calendar.html#setLenient-boolean-) 
>日付/時間の解釈を厳密に行うかどうかを設定します。（中略）デフォルトは非厳密です。

これが、 非厳密を許容する場合`true`、厳密に行う場合`false`を引数に渡す。
lenient という単語が「ゆるい」という意味のため、
「ゆるい（true）」と「ゆるくない（false）」という引数の対応となる。

#実在日付チェックはsetLenient()で手軽に実装可能
まとめ的に繰り返すと、

```java
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
sdf.setLenient(false);
```
だけで厳密な日付解析が行わる。