イマカラブログ

イマカラメガネの中の人が、芝居と関係あることないこと好き勝手に書くブログ。

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桁ぐらいならこっちのほうがいいな。 これなら小数点で終わっている場合に解析失敗してくれるし。

とりあえず目先の用途としてはこれでいいか。