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

簡単な日時のフォーマット変換ユーティリティを書こうとした時に、
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するのは問題無かった。

```java
        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しようとしたところで問題発生。

```java
        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できないそうです。
なんじゃそりゃ～

他にも、"yyyyMMddHHmmss<b>SS</b>"とか"yyyyMMddHHmmss<b>S</b>"もダメだそうで...
逆に、"yyyyMMddHHmmss<b>.</b>SSS"等、
ミリ秒の直前にパターン文字以外の文字やら記号が入っていれば問題なく動きます。

# このバグはいつ直るの？
stackoverflowには、
このバグの修正は少なくともJava9以降になりそうだと書いてありますね...
そういうもんなんでしょうか...

【2017年5月29日追記】
JDK Bug Systemでのstatusが、Resolvedになっていました。
ただし、やはり修正の反映はJava 9になるようです。

# 対策
バグが直るまでの暫定対応として、前述のstackoverflowに投稿されている内容をまとめています。
ミリ秒部（S）が3桁の場合と、1桁或いは2桁の場合で、対応が異なります。
しかし3桁の場合は良いのですが、1、2桁の場合は何か他に良い対策はないものだろうか。
強引な実装はもちろん、仕事で実装する場合はサードパーティ製のライブラリとか可能なら避けたいですよね...

## ミリ秒部が3桁の場合
### フォーマッタの定義を工夫する

```java
    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桁も行けるかと思えば...

```java
    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桁の場合は同じように桁がずれる。

```java
        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桁の場合の実装例）

```java

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. サードパーティ製のライブラリを使用する。
- [Joda-Time](http://www.joda.org/joda-time/)

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

- [Time4J](https://github.com/MenoData/Time4J)

```java

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日追記】
↓とてもシンプルに解決されている方がいました。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/yoshiyoshifujii">@yoshiyoshifujii</a> フォーマット変更はきびしいですねー。とりあえずドット入れてパースするユーティリティな関数を作ってしのぎましたが...</p>&mdash; Yohei Tsuji (@crossroad0201) <a href="https://twitter.com/crossroad0201/status/755541710281728000">2016年7月19日</a></blockquote>
<script async src="//platform.twitter.com/widgets.js" charset="utf-8"></script>