56
44

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における日付文字列の書式チェック方法

Last updated at Posted at 2016-12-04

Javaにおける日付文字列の書式・日付チェックは落とし穴も多く、自分の身の回りでもそれによる障害等も発生しているので整理してみます。
例えば、ある文字列が、yyyy/MM/dd(e.g. 2016/01/01)という書式かつ有効な日付であることをチェックしたい場合を考えてみます。

Java8の場合

Date and Time APIを使うのがいいと思います。
以下の例では9/31は存在しないため例外が発生します。

LocalDate.parse("2016/09/31",
    DateTimeFormatter.ofPattern("uuuu/MM/dd").withResolverStyle(ResolverStyle.STRICT));

または

DateTimeFormatter.ofPattern("uuuu/MM/dd")
    .withResolverStyle(ResolverStyle.STRICT)
    .parse("2016/09/31", LocalDate::from);

※注意すべきこと

  • ResolverStyle.STRICT を指定することを忘れないようにしないといけません。以下のように指定しない場合、例外は発生せず、9/31が9/30とパースされます。

     LocalDate.parse("2016/09/31", DateTimeFormatter.ofPattern("uuuu/MM/dd"));
    

ResolverStyleについてはAPIドキュメント https://docs.oracle.com/javase/jp/8/docs/api/java/time/format/ResolverStyle.html に記載がある通り、デフォルトでは ResolverStyle.SMART として扱われ、数字は有効範囲におさまっている必要があるものの(13月とかはダメ)、31が存在しない月なら月末としてパースされます。

Java7以下の場合

方法1. 一旦、Date型に変換して書式指定で文字列にしたものとチェック対象文字列を文字列比較する

こういうやつです。

String value = "2016/09/31";
SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd");
df.setLenient(false);
Date parsedDate = df.parse(value);
return df.format(parsedDate).equals(value);

標準APIのみで実装でき、また、日付ライブラリAPIの細かい仕様がわかっていなくても、これが通れば抜けはないはずというのがわかりやすい方法だと思います。

方法2. Joda-Timeを使う

Java7以前までJavaにおける日付ライブラリの定番的な存在だったJoda-Time http://www.joda.org/joda-time/ を使う方法です。

new DateTimeFormatterBuilder()
    .appendFixedDecimal(DateTimeFieldType.year(), 4)
    .appendLiteral('/')
    .appendFixedDecimal(DateTimeFieldType.monthOfYear(), 2)
    .appendLiteral('/')
    .appendFixedDecimal(DateTimeFieldType.dayOfMonth(), 2)
    .toFormatter()
    .parseLocalDate("2016/09/31");

※注意すべきこと

  • Date and Time APIのように

     LocalDate.parse("2016/09/31", DateTimeFormat.forPattern("yyyy/MM/dd"));
    

    または、

     DateTimeFormat.forPattern("yyyy/MM/dd").parseLocalDate("2016/09/31");
    

と書いた場合は2016/09/1のように有効な日付でゼロパディングがされていない文字列がエラーになりません。これをチェックするためにDateTimeFormatterBuilderを使う必要があります。この辺、Date and Time APIはJoda-Timeから微妙に仕様が変わっていてややこしい感じがします。
なお、既存でJoda-Timeを使っておらず、将来的にJava8への移行も考えている場合であれば、Joda-Timeの開発者にして、Date and Time API(JSR-310)のスペックリードであるStephen Colebourne氏が、Java8 Date and Time APIのクラスをJava6/7向けにバックポートしたライブラリとしてThreeTen-Backport http://www.threeten.org/threetenbp/ を出していますので、そちらを使うのもいいと思います。

ダメなやり方

1. SimpleDateFormatでsetLenientをfalseにする

setLenient自体を知らなくてハマる人も結構見かける気がしますが、setLenientをfalseにしてもダメです。
APIドキュメント http://docs.oracle.com/javase/jp/7/api/java/text/SimpleDateFormat.html#parse(java.lang.String,%20java.text.ParsePosition)解析では、文字列の最後までのすべての文字が使用されるとは限らない と記載されている通り、SimpleDateFormatは日付と解析できる文字列の後ろに余計な不正文字がついていても無視します。

DateFormat df = new SimpleDateFormat("yyyy/MM/dd");
df.setLenient(false);
df.parse("2016/09/1a");

そういうわけで、これが通ってしまうのでダメです(パース結果は2016/09/01になります)。

2. Apache Commons Validatorを使う

名前からすると、GenericValidator#isDate(String value, String datePattern, boolean strict) が使えそうに思えますが、実装としては、setLenient(false)してさらに、書式の文字数 == 値の文字数チェックをしているだけで、2016/12/1aとかが通ってしまうのでダメです。
cf.) https://github.com/apache/commons-validator/blob/trunk/src/main/java/org/apache/commons/validator/DateValidator.java#L71

3. Apache Commons Langを使う

org.apache.commons.lang.time.DateUtils#parseDateStrictly が名前からするとStrictlyとかついていて厳密なチェックをしてくれそうに思えますが、これも実装としては、setLenient(false)して、ParsePositionを使ってどこまで読んだかチェックし、チェック対象文字列の長さと比較していますが、2016/09/1や2016/09/001が通ってしまうのでダメです。
cf.) https://github.com/apache/commons-lang/blob/LANG_2_6/src/main/java/org/apache/commons/lang/time/DateUtils.java#L323
version3でパッケージも変わり実装も変わったorg.apache.commons.lang3.time.DateUtils#parseDateStrictly も同じく、2016/09/1や2016/09/001が通ってしまうのでやはりダメです。

他にも比較対象を書式文字列の長さにした http://d.hatena.ne.jp/syttru/20100615/1276615231 のようなやり方でも、2016/9/001が通ってしまうため、やはりダメです。

ゼロパディングがありでもなしでもOKな仕様にするなら?

Date and Time APIの場合

以下のようにDateTimeFormatterBuilderを使います。

LocalDate.parse("2016/9/30", 
    new DateTimeFormatterBuilder()
        .appendValue(ChronoField.YEAR, 4, 4, SignStyle.NEVER)
        .appendLiteral('/')
        .appendValue(ChronoField.MONTH_OF_YEAR, 1, 2, SignStyle.NEVER)
        .appendLiteral('/')
        .appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NEVER)
        .toFormatter()
        .withResolverStyle(ResolverStyle.STRICT));

※注意すべきこと
以下のように"uuuu/M/d"だと2016/00009/000030とかでも通っちゃうので注意が必要です。

LocalDate.parse("2016/00009/000030",
    DateTimeFormatter.ofPattern("uuuu/M/d").withResolverStyle(ResolverStyle.STRICT));

Joda-Timeの場合

上述の通り、

LocalDate.parse("2016/9/30", DateTimeFormat.forPattern("yyyy/MM/dd"));

または、

DateTimeFormat.forPattern("yyyy/MM/dd").parseLocalDate("2016/9/30");

のように書けばOKです。

56
44
1

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
56
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?