2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

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

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();
}
2019/07/20 12:34:56

入力したStringの値通りにparseとformatが実行される。

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

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();
}
2022/05/14 01:13:12

動作で言えば、ParseExceptionが吐かれることなく、それっぽく変換が成されるのだ。

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

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になるのでは?というのが幻想のようなので、確認。

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);
...
   (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);

だけで厳密な日付解析が行わる。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?