簡単な日時のフォーマット変換ユーティリティを書こうとした時に、
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です。
詳しくは下記参照。
- 公式APIドキュメント
https://docs.oracle.com/javase/jp/8/docs/api/java/time/format/DateTimeFormatter.html - Hishidamaさんのブログ
http://www.ne.jp/asahi/hishidama/home/tech/java/DateTimeFormatter.html
やろうとしたこと・起きたこと
下記のように、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日追記】
↓とてもシンプルに解決されている方がいました。
@yoshiyoshifujii フォーマット変更はきびしいですねー。とりあえずドット入れてパースするユーティリティな関数を作ってしのぎましたが...
— Yohei Tsuji (@crossroad0201) 2016年7月19日