Edited at

[Java8 バグ] DateTimeFormatterで"yyyyMMddHHmmssSSS"がparseできない。

More than 1 year has passed since last update.

簡単な日時のフォーマット変換ユーティリティを書こうとした時に、

Java8のバグに出会ってハマったので記事にします。

※この記事内容は、ほぼ下記リンク先のstackoverflowからの転載です。

 自分はこのバグで結構ハマったので、

 Qiitaに記事があればもっと早く原因がわかったのでは...という思いで書いています。

 http://stackoverflow.com/questions/22588051/is-java-time-failing-to-parse-fraction-of-second


実行環境

この記事内のコードを実行した時の、私のPC環境です。


  • OS:Windows7 64bit

  • Javaのバージョン:Java SE 8u77


DateTimeFormatter

DateTimeFormatterはJava8で導入されたAPIです。

詳しくは下記参照。


やろうとしたこと・起きたこと

下記のように、yyyyMMddHHmmss形式の日時文字列(ミリ秒なし)をLocalDateTimeにparseするのは問題無かった。

        String input = "20111203123456";

DateTimeFormatter dtf = DateTimeFormatter.ofPattern( "yyyyMMddHHmmss");
LocalDateTime localDateTime = LocalDateTime.parse(input, dtf);
System.out.println(localDateTime);//output: 2011-12-03T12:34:56

しかし、同じノリでyyyyMMddHHmmssSSS形式の日時文字列(ミリ秒含む)をparseしようとしたところで問題発生。

        String input = "20111203123456789";

DateTimeFormatter dtf = DateTimeFormatter.ofPattern( "yyyyMMddHHmmssSSS");
LocalDateTime localDateTime = LocalDateTime.parse(input, dtf);//Error!
System.out.println(localDateTime);

実行時に下記のエラーが...!?

Exception in thread "main" java.time.format.DateTimeParseException: Text '20111203123456789' could not be parsed at index 0

at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949)
at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)
at java.time.LocalDateTime.parse(LocalDateTime.java:492)
at Main.main(Main.java:11)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

えぇ...

なんで...??

ミリ秒のパターンの指定の仕方がおかしいのか!?とか悩み、APIドキュメント見ながら試行錯誤しましたが一向に解決せず...


原因

途方にくれながらググッていたら、前述のstackoverflowに辿りつきました。

JDKのバグが原因でした。

下記、JDK Bug Systemにて既に報告されていました。

https://bugs.openjdk.java.net/browse/JDK-8031085

"yyyyMMddHHmmssSSS"のパターンはparseできないそうです。

なんじゃそりゃ~

他にも、"yyyyMMddHHmmssSS"とか"yyyyMMddHHmmssS"もダメだそうで...

逆に、"yyyyMMddHHmmss.SSS"等、

ミリ秒の直前にパターン文字以外の文字やら記号が入っていれば問題なく動きます。


このバグはいつ直るの?

stackoverflowには、

このバグの修正は少なくともJava9以降になりそうだと書いてありますね...

そういうもんなんでしょうか...

【2017年5月29日追記】

JDK Bug Systemでのstatusが、Resolvedになっていました。

ただし、やはり修正の反映はJava 9になるようです。


対策

バグが直るまでの暫定対応として、前述のstackoverflowに投稿されている内容をまとめています。

ミリ秒部(S)が3桁の場合と、1桁或いは2桁の場合で、対応が異なります。

しかし3桁の場合は良いのですが、1、2桁の場合は何か他に良い対策はないものだろうか。

強引な実装はもちろん、仕事で実装する場合はサードパーティ製のライブラリとか可能なら避けたいですよね...


ミリ秒部が3桁の場合


フォーマッタの定義を工夫する

    String input = "20111203123456789";

DateTimeFormatter dtf =
new DateTimeFormatterBuilder()
.appendPattern("yyyyMMddHHmmss")
.appendValue(ChronoField.MILLI_OF_SECOND, 3)
.toFormatter();
LocalDateTime localDateTime = LocalDateTime.parse(input, dtf);
System.out.println(localDateTime); //output: 2011-12-03T12:34:56.789


ミリ秒部が1桁または2桁の場合

3桁の時と同じノリで2桁も行けるかと思えば...

    String input = "2011120312345678";

DateTimeFormatter dtf =
new DateTimeFormatterBuilder()
.appendPattern("yyyyMMddHHmmss")
.appendValue(ChronoField.MILLI_OF_SECOND, 2)
.toFormatter();
LocalDateTime localDateTime = LocalDateTime.parse(input, dtf);
System.out.println(localDateTime); //output: 2011-12-03T12:34:56.078

エラーにはなりませんが、出力結果をよく見るとミリ秒部分が1桁ずれてます...

//expected  2011-12-03T12:34:56.780

//actual 2011-12-03T12:34:56.078
^^^

これではダメだ。

ちなみに、SimpleDateFormatを使用しても、ミリ秒が2桁の場合は同じように桁がずれる。

        String input = "2011120312345678";

SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSS");
Date d = sdf.parse(input);
System.out.println(d.toInstant()); // 2011-12-03T03:34:56.078Z

ミリ秒部分が1桁または2桁の場合は、下記の対策のどちらかで対応する必要がある。


1. 強引に桁をずらす。

ミリ秒が2桁の場合の実装例)


String input = "2011120312345678";
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
int len = input.length();
LocalDateTime ldt = LocalDateTime.parse(input.substring(0, len - 2), dtf);
int millis = Integer.parseInt(input.substring(len - 2)) * 10;
ldt = ldt.plus(millis, ChronoUnit.MILLIS);
System.out.println(ldt); //output: 2011-12-03T12:34:56.780


2. サードパーティ製のライブラリを使用する。

String input = "2011120312345678"; 

DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyyMMddHHmmssSS");
System.out.println(dtf.parseLocalDateTime(input)); //output: 2011-12-03T12:34:56.780


String input = "2011120312345678";
ChronoFormatter<PlainTimestamp> f =
ChronoFormatter.ofTimestampPattern("yyyyMMddHHmmssSS", PatternType.CLDR, Locale.ROOT);
System.out.println(f.parse(input)); //output: 2011-12-03T12:34:56.780

うーん微妙ですよね・・・

【2017年5月29日追記】

↓とてもシンプルに解決されている方がいました。