#存在しない日付がparseされる
最小限のコーディングでのサンプル。
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();
}
2019/07/20 12:34:56
入力したString
の値通りにparseとformatが実行される。
ここで、String
の値を2020/27/72 72:72:72
に設定したときの動きを検証する。
大体パースするような処理では、この値のように、実在しない日時、
カレンダーや時計で表示され得ない日時のデータは、
異常入力として、パースを失敗させたい、はずだ。
この記事はそういう要件向けに書いている。
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();
}
2022/05/14 01:13:12
動作で言えば、ParseException
が吐かれることなく、それっぽく変換が成されるのだ。
#そんなデータをparseさせたくない
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();
}
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)での記載を検証する。
##入力値の桁数のチェック仕様
フォーマットとして、"yyyy/MM/dd HH:mm:ss"
を指定しているのだから、
MMは2桁、ddも2桁... じゃないとNGになるのでは?というのが幻想のようなので、確認。
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);
...
(ParseExceptionは発生しない)
この挙動については、APIで記載されている。
数値: フォーマット時に、パターン文字の数は最小桁数です。これより短い数値は、この桁数までゼロ埋めされます。解析には、2つの隣接するフィールドを区切る必要がないかぎり、パターン文字の数は無視されます。
文字の数は無視されるのだ。
※yyyyとyy、 MMとMMM など、そもそも「数値」に該当するかどうかが変化するものについては、注意が必要だが、
数値として解釈されるフィールドについては、パターン文字の数は、解釈に影響しない。
##入力値の実在日時チェック仕様
parse(String)
のデフォルトでの日付チェック(API上では「厳密な解析」といった表現)については、
SimpleDateFormat のスーパークラスである、 DateFormatのAPIに記載があった。
デフォルト値では、解析は厳密ではありません。入力が、このオブジェクトのフォーマット・メソッドで使用される形式ではないが、日付として解析可能であれば、解析は正常に行われます。
SimpleDateFormat.parse(String) → DateFormat.parse(String) → DateFormat.parse(String, ParsePosition)
という流れで記載に行きつくことができた。
このAPIの記載、上の一文に続けて、このように書かれていた。
クライアントは、setLenient(false)を呼び出すことによって、このフォーマットを厳密に要求できます。
なるほど、APIって最強のドキュメントだね。
##setLenient(false)とは何者
結論から言えば、Calendar
クラスのメソッドである。
Calendar.setLinient(boolean)
日付/時間の解釈を厳密に行うかどうかを設定します。(中略)デフォルトは非厳密です。
これが、 非厳密を許容する場合true
、厳密に行う場合false
を引数に渡す。
lenient という単語が「ゆるい」という意味のため、
「ゆるい(true)」と「ゆるくない(false)」という引数の対応となる。
#実在日付チェックはsetLenient()で手軽に実装可能
まとめ的に繰り返すと、
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
sdf.setLenient(false);
だけで厳密な日付解析が行わる。