DateTimeFormatterで可変幅のミリ秒の解析
おさらい
前回の記事の続きです。
ミリ秒の解析のためにDateTimeFormatter
を使ってパターン文字"S"を1文字で指定したら、
解析対象のミリ秒が2桁以上あると解析に失敗した。
// NGな例 DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.S"); LocalDateTime ldt = dtf.parse("2018-08-12T12:34:56.12", LocalDateTime::from); System.out.println(ldt); // -> java.time.format.DateTimeParseException: Text '2018-08-12T12:34:56.12' could not be parsed, unparsed text found at index 21
原因
日付や時、分、秒はパターン文字を1文字だけ書けば複数桁の解析ができるのになぜ?
と調べてDateTimeFormatterBuilder#appendPattern(String)
のJavadocに次の記述を見つけた。
パターン文字の数によってフォーマットが決まります。ユーザーに焦点を合わせたパターンの説明については、DateTimeFormatterを参照してください。次の表は、パターン文字がどのようにビルダーにマップされるかを定義しています。
Pattern Count Equivalent builder methods H 1 appendValue(ChronoField.HOUR_OF_DAY) HH 2 appendValue(ChronoField.HOUR_OF_DAY, 2) m 1 appendValue(ChronoField.MINUTE_OF_HOUR) mm 2 appendValue(ChronoField.MINUTE_OF_HOUR, 2) s 1 appendValue(ChronoField.SECOND_OF_MINUTE) ss 2 appendValue(ChronoField.SECOND_OF_MINUTE, 2) S..S 1..n appendFraction(ChronoField.NANO_OF_SECOND, n, n, false)
※表は抜粋
この表を見るとappendPattern
に指定したパターン文字"S"は、内部でappendFraction
の呼び出しに置き換えられていて、最小幅と最大幅が"S"を書いた文字数(n)に指定されている。
だから、パターン文字"S"を1文字だけ書いて解析対象のミリ秒が1文字でない場合は解析に失敗したのか。
一方、時(H)、分(m)、秒(s)などを1文字だけ書いた場合appendValue
が内部で使用され、このメソッドは可変幅の文字も受け付けるので、解析対象が1桁でも複数桁でも解析に失敗しない。
なぜ、ミリ秒だけこんな仕様なんだ?他のフィールドと同じように1文字だけなら可変幅にすればいいじゃないかと疑問に感じたが、 冷静に考えると、解析対象としてミリ秒以外のフィールドを1文字幅に制限するということは通常考えられないが、ミリ秒(秒の小数部)は1文字幅に制限したいという可能性はある。 その可能性を考慮すると、この仕様は確かに妥当かと思い直した。
対策
とりあえず仕様に納得はしたものの困ったことには変わりない。
今やりたいことは「ミリ秒は可変幅で、それ以外のフィールドは固定幅で日時を解析」である。
みんなどうやってるのかググったところ、次のようにやってる人が多そう。
DateTimeFormatterBuilder dtfBldr = new DateTimeFormatterBuilder(); DateTimeFormatter dtf = dtfBldr .appendPattern("yyyy-MM-dd'T'HH:mm:ss") .appendFraction(ChronoField.NANO_OF_SECOND, 0, 3, true) .toFormatter(); LocalDateTime ldt = dtf.parse("2018-08-01T12:34:56.12", LocalDateTime::from); System.out.println(ldt); // -> 2018-08-01T12:34:56.120
うーん。ちょっとめんどい。
めんどいし、解析対象が小数点で終わっている場合に失敗してくれないのがちょっと嫌だ。
もうちょっとスマートにできないかと調べたら Stack Overflow で次のようにやってるのを見つけた。
DateTimeFormatter dtf = DateTimeFormatter .ofPattern("yyyy-MM-dd'T'HH:mm:ss[.SSS][.SS][.S]"); LocalDateTime ldt = dtf.parse("2018-08-01T12:34:56.12", LocalDateTime::from); System.out.println(ldt); // -> 2018-08-01T12:34:56.120
なるほど。オプションで長い幅からマッチングさせてるのか。頭いい。
小数部の桁数が多いときは先の方法でやったほうがいいけど、3桁ぐらいならこっちのほうがいいな。 これなら小数点で終わっている場合に解析失敗してくれるし。
とりあえず目先の用途としてはこれでいいか。