CLOVER🍀

That was when it all began.

DateTimeFormatterでパースとフォーマット時の桁数や空白について調べる

これは、なにをしたくて書いたもの?

DateTimeFormatterで文字列として表現された日時をパースする時に桁数や空白の扱いについてあまり意識していなかったので、
ちょっと見てみることにしました。

DateTimeFormatter (Java SE 17 & JDK 17)

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.5 2022-10-18
OpenJDK Runtime Environment (build 17.0.5+8-Ubuntu-2ubuntu122.04)
OpenJDK 64-Bit Server VM (build 17.0.5+8-Ubuntu-2ubuntu122.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.7 (b89d5959fcde851dcb1c8946a785a163f14e1e29)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.5, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.15.0-58-generic", arch: "amd64", family: "unix"

準備

確認は、テストコードで行います。必要なMaven依存関係等はこちら。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.9.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.24.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M7</version>
            </plugin>
        </plugins>
    </build>

パースする

最初はパースから。

以下をテストコードの雛形にしていきます。

src/test/java/org/littlewings/datetimeformatter/ParseTest.java

package org.littlewings.datetimeformatter;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.util.Calendar;
import java.util.GregorianCalendar;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class ParseTest {

    // ここに、テストを書く!!
}
SimpleDateFormatとDateTimeFormatterにおける、パース時の桁数に対する振る舞い

このエントリーを書くきっかけになった話ですが。

まずは昔ながらのSimpleDateFormatを素直に使ってみます。日付と日時に対して、それぞれパース。

    @Test
    void simpleDateFormat() throws ParseException {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        simpleDateFormat.setLenient(true);

        Calendar date = new GregorianCalendar(2022, 9, 25);

        assertThat(simpleDateFormat.parse("2022-10-25"))
                .isEqualTo(date.getTime());

        SimpleDateFormat simpleDateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        simpleDateTimeFormat.setLenient(true);

        Calendar dateTime = new GregorianCalendar(2022, 9, 25, 16, 30, 25);

        assertThat(simpleDateTimeFormat.parse("2022-10-25 16:30:25"))
                .isEqualTo(dateTime.getTime());
    }

続いて、DateTimeFormatter。

    @Test
    void dateTimeFormatter() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDate.parse("2022-10-25", dateFormatter))
                .isEqualTo(LocalDate.of(2022, 10, 25));

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDateTime.parse("2022-10-25 16:30:25", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2022, 10, 25, 16, 30, 25));
    }

まあ、ふつうだと思います。

次に、桁数が少ないデータを渡してみます。

SimpleDateFormat。

    @Test
    void shortWidthSimpleDateFormat() throws ParseException {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        simpleDateFormat.setLenient(true);

        Calendar date = new GregorianCalendar(2023, 0, 5);

        assertThat(simpleDateFormat.parse("2023-1-5"))
                .isEqualTo(date.getTime());

        SimpleDateFormat simpleDateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        simpleDateTimeFormat.setLenient(true);

        Calendar dateTime = new GregorianCalendar(2023, 0, 5, 2, 3, 4);

        assertThat(simpleDateTimeFormat.parse("2023-1-5 2:3:4"))
                .isEqualTo(dateTime.getTime());
    }

問題なくパースできます。

DateTimeFormatter。

    @Test
    void shortWidthDateTimeFormatter() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT);
        assertThatThrownBy(() -> LocalDate.parse("2023-1-5", dateFormatter))
                .isInstanceOf(DateTimeParseException.class)
                .hasMessage("Text '2023-1-5' could not be parsed at index 5");

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss").withResolverStyle(ResolverStyle.STRICT);
        assertThatThrownBy(() -> LocalDateTime.parse("2023-1-5 2:3:4", dateTimeFormatter))
                .isInstanceOf(DateTimeParseException.class)
                .hasMessage("Text '2023-1-5 2:3:4' could not be parsed at index 5");
    }

こちらは、パースに失敗します。これを回避するにはどうしたら?というのが、今回のエントリーを書いたきっかけですね。

当たり前ですが、足りない桁を0埋めすればパースには失敗しません。

    @Test
    void shortWidthDateTimeFormatter2() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDate.parse("2023-01-05", dateFormatter))
                .isEqualTo(LocalDate.of(2023, 1, 5));

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDateTime.parse("2023-01-05 02:03:04", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2023, 1, 5, 2, 3, 4));
    }

ちなみに、空白を置いてもダメです。

    @Test
    void shortWidthDateTimeFormatter3() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu/MM/dd").withResolverStyle(ResolverStyle.STRICT);
        assertThatThrownBy(() -> LocalDate.parse("2023/ 1/ 5", dateFormatter))
                .isInstanceOf(DateTimeParseException.class)
                .hasMessage("Text '2023/ 1/ 5' could not be parsed at index 5");

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu/MM/dd HH:mm:ss").withResolverStyle(ResolverStyle.STRICT);
        assertThatThrownBy(() -> LocalDateTime.parse("2023/ 1/ 5  2: 3: 4", dateTimeFormatter))
                .isInstanceOf(DateTimeParseException.class)
                .hasMessage("Text '2023/ 1/ 5  2: 3: 4' could not be parsed at index 5");
    }

このパターンの時は、区切り文字を/にしています。

パターンを1文字にする

それで、こういう時にパースしたかったらどうするかというと、パターンを1文字にするとパースできるようになります。

    @Test
    void shortWidthShortDateTimeFormatter() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu-M-d").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDate.parse("2022-10-25", dateFormatter))
                .isEqualTo(LocalDate.of(2022, 10, 25));
        assertThat(LocalDate.parse("2023-1-5", dateFormatter))
                .isEqualTo(LocalDate.of(2023, 1, 5));

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu-M-d H:m:s").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDateTime.parse("2022-10-25 16:30:25", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2022, 10, 25, 16, 30, 25));
        assertThat(LocalDateTime.parse("2023-1-5 2:3:4", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2023, 1, 5, 2, 3, 4));
    }
オプションで空白を置く

DateTimeFormatterには「オプション」というものがあり、[]で表現します。

オプションのセクション: オプションのセクションのマーカーは、DateTimeFormatterBuilder.optionalStart()およびDateTimeFormatterBuilder.optionalEnd()の呼出しとまったく同様に機能します。

DateTimeFormatter (Java SE 17 & JDK 17)

DateTimeFormatterBuilder.html#optionalStartの説明を見ると、こんなことが書かれています。

書式設定の出力には、オプションのセクションを含めることができ、それらは入れ子にすることができます。 オプションのセクションは、このメソッドの呼出しによって始まり、optionalEnd()の呼出しかビルダー・プロセスの終了によって終わります。

オプションのセクションに含まれるすべての要素は、オプションとして扱われます。 書式設定時は、セクション内のすべての要素に関するデータがTemporalAccessorで使用可能な場合のみ、セクションが出力されます。 解析時は、解析された文字列からセクション全体が欠けている場合があります。

DateTimeFormatterBuilder#optionalStart)

DateTimeFormatterの場合は、[]で囲った範囲がオプションになるということみたいです。

試してみましょう。

    @Test
    void shortWidthOptionalSpaceDateTimeFormatter() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu/[ ]M/[ ]d").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDate.parse("2022/10/25", dateFormatter))
                .isEqualTo(LocalDate.of(2022, 10, 25));
        assertThat(LocalDate.parse("2023/1/5", dateFormatter))
                .isEqualTo(LocalDate.of(2023, 1, 5));
        assertThat(LocalDate.parse("2023/ 1/ 5", dateFormatter))
                .isEqualTo(LocalDate.of(2023, 1, 5));
        assertThat(LocalDate.parse("2023/01/05", dateFormatter))
                .isEqualTo(LocalDate.of(2023, 1, 5));

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu/[ ]M/[ ]d [ ]H:[ ]m:[ ]s").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDateTime.parse("2022/10/25 16:30:25", dateTimeFormatter))
                        .isEqualTo(LocalDateTime.of(2022, 10, 25, 16, 30, 25));
        assertThat(LocalDateTime.parse("2023/1/5 2:3:4", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2023, 1, 5, 2, 3, 4));
        assertThat(LocalDateTime.parse("2023/ 1/ 5  2: 3: 4", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2023, 1, 5, 2, 3, 4));
        assertThat(LocalDateTime.parse("2023/01/05 02:03:04", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2023, 1, 5, 2, 3, 4));
    }

空白があってもなくてもよくなり、2桁であってもパースできますね。1桁でも2桁でもパースできるのは、パターンを1文字にしているからで
あくまでオプションなのは空白なのですが。

オプションをもう少し

もう少し、オプションのパターンを試してみましょう。

日時の部分をオプションにしてみます。

    @Test
    void optionalDateTimeFormatter() {
        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu-MM-dd[ HH:mm:ss]").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDate.parse("2022-10-25", dateTimeFormatter))
                .isEqualTo(LocalDate.of(2022, 10, 25));
        assertThat(LocalDate.parse("2023-01-05", dateTimeFormatter))
                .isEqualTo(LocalDate.of(2023, 1, 5));
        assertThat(LocalDateTime.parse("2022-10-25 16:30:25", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2022, 10, 25, 16, 30, 25));
        assertThatThrownBy(() -> LocalDateTime.parse("2023-01-05 2:3:4", dateTimeFormatter))
                .isInstanceOf(DateTimeParseException.class)
                .hasMessage("Text '2023-01-05 2:3:4' could not be parsed, unparsed text found at index 10");
    }

オプションをネストさせることもできます。こちらは秒の部分をオプションにしています。

    @Test
    void nestedOptionalDateTimeFormatter() {
        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu-MM-dd[ HH:mm[:ss]]").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDate.parse("2022-10-25", dateTimeFormatter))
                .isEqualTo(LocalDate.of(2022, 10, 25));
        assertThat(LocalDate.parse("2023-01-05", dateTimeFormatter))
                .isEqualTo(LocalDate.of(2023, 1, 5));
        assertThat(LocalDateTime.parse("2022-10-25 16:30:25", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2022, 10, 25, 16, 30, 25));
        assertThat(LocalDateTime.parse("2023-01-05 02:03:04", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2023, 1, 5, 2, 3, 4));
        assertThat(LocalDateTime.parse("2022-10-25 16:30:25", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2022, 10, 25, 16, 30, 25));
        assertThat(LocalDateTime.parse("2023-01-05 02:03", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2023, 1, 5, 2, 3, 0));
    }
パディング

少し観点を変えて、パディングを指定する場合はpを使います。

    @Test
    void shortWidthPaddingDateTimeFormatter() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu/ppM/ppd").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDate.parse("2022/10/25", dateFormatter))
                .isEqualTo(LocalDate.of(2022, 10, 25));
        assertThat(LocalDate.parse("2023/ 1/ 5", dateFormatter))
                .isEqualTo(LocalDate.of(2023, 1, 5));
        assertThat(LocalDate.parse("2023/01/05", dateFormatter))
                .isEqualTo(LocalDate.of(2023, 1, 5));

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu/ppM/ppd ppH:ppm:pps").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDateTime.parse("2022/10/25 16:30:25", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2022, 10, 25, 16, 30, 25));
        assertThat(LocalDateTime.parse("2023/ 1/ 5  2: 3: 4", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2023, 1, 5, 2, 3, 4));
        assertThat(LocalDateTime.parse("2023/01/05 02:03:04", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2023, 1, 5, 2, 3, 4));
    }

ppで、2桁であることを求めていますが、桁数に満たない場合は空白が必須になります。

なので、以下のように桁数が不足する場合にパディングしていないとパースできません。

    @Test
    void shortWidthPaddingDateTimeFormatter2() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu/ppM/ppd").withResolverStyle(ResolverStyle.STRICT);
        assertThatThrownBy(() -> LocalDate.parse("2023/1/5", dateFormatter))
                .isInstanceOf(DateTimeParseException.class)
                .hasMessage("Text '2023/1/5' could not be parsed at index 10");

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu/ppM/ppd ppH:ppm:pps").withResolverStyle(ResolverStyle.STRICT);
        assertThatThrownBy(() -> LocalDateTime.parse("2023/1/5 2:3:4", dateTimeFormatter))
                .isInstanceOf(DateTimeParseException.class)
                .hasMessage("Text '2023/1/5 2:3:4' could not be parsed at index 10");
    }
1文字のパターンは長さを見ていない

こう見ると、1文字のパターンとオプションの組み合わせが柔軟で良さそうに見えますが、1文字のパターンは長さを見ていない感じが
しますね。

以下のようなパターンもパースできてしまいます。

    @Test
    void looseDateTimeFormatter() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu-M-d").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDate.parse("2023-00001-00005", dateFormatter))
                .isEqualTo(LocalDate.of(2023, 1, 5));

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu-M-d H:m:s").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDateTime.parse("2023-00001-00005 00002:00003:00004", dateTimeFormatter))
                .isEqualTo(LocalDateTime.of(2023, 1, 5, 2, 3, 4));
    }

この桁数を厳密に制御しようと思うと、DateTimeFormatter#ofPatternでパターン文字列から作るのではなく、
DateTimeFormatterBuilderクラスのappendValue`メソッドを使うことになりそうです。

厳密解析モードでは、解析される桁数の最小はminWidth、最大はmaxWidthです。 非厳密解析モードでは、解析される桁数の最小は1、最大は19です(隣接値解析によって制限される場合を除く)。

DateTimeFormatterBuilder#appendValue)

今回は、DateTimeFormatterBuilderクラスは扱いませんが。

フォーマットする

パースができたので、次はフォーマットしてみます。

こちらもテストコードで確認するので、雛形から。

src/test/java/org/littlewings/datetimeformatter/FormatTest.java

package org.littlewings.datetimeformatter;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.ResolverStyle;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class FormatTest {

    // ここに、テストを書く!!
}
シンプルなパターンでフォーマットする

まずは、シンプルなパターンでフォーマットしてみます。

    @Test
    void format() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu-MM-dd").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDate.of(2022, 10, 25).format(dateFormatter))
                .isEqualTo("2022-10-25");
        assertThat(LocalDate.of(2023, 1, 5).format(dateFormatter))
                .isEqualTo("2023-01-05");

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDateTime.of(2022, 10, 25, 16, 30, 25).format(dateTimeFormatter))
                .isEqualTo("2022-10-25 16:30:25");
        assertThat(LocalDateTime.of(2023, 1, 5, 2, 3, 4).format(dateTimeFormatter))
                .isEqualTo("2023-01-05 02:03:04");
    }

この場合、桁数に満たない値は0埋めされます。

パターン文字を1にする

次に、パターン文字を1文字にしてみます。

    @Test
    void shortFormat() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu-M-d").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDate.of(2022, 10, 25).format(dateFormatter))
                .isEqualTo("2022-10-25");
        assertThat(LocalDate.of(2023, 1, 5).format(dateFormatter))
                .isEqualTo("2023-1-5");

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu-M-d H:m:s").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDateTime.of(2022, 10, 25, 16, 30, 25).format(dateTimeFormatter))
                .isEqualTo("2022-10-25 16:30:25");
        assertThat(LocalDateTime.of(2023, 1, 5, 2, 3, 4).format(dateTimeFormatter))
                .isEqualTo("2023-1-5 2:3:4");
    }

この場合、桁数がそのまま文字列長に反映される感じになりますね。

オプションで空白を置いてみる

パースする時に、空白をオプションにするようにしてみましたが、これだとどうなるでしょう。

    @Test
    void optionalShortFormat() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu/[ ]M/[ ]d").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDate.of(2022, 10, 25).format(dateFormatter))
                .isEqualTo("2022/ 10/ 25");
        assertThat(LocalDate.of(2023, 1, 5).format(dateFormatter))
                .isEqualTo("2023/ 1/ 5");

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu/[ ]M/[ ]d [ ]H:[ ]m:[ ]s").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDateTime.of(2022, 10, 25, 16, 30, 25).format(dateTimeFormatter))
                .isEqualTo("2022/ 10/ 25  16: 30: 25");
        assertThat(LocalDateTime.of(2023, 1, 5, 2, 3, 4).format(dateTimeFormatter))
                .isEqualTo("2023/ 1/ 5  2: 3: 4");
    }

常に空白が確保されるようになりました…。

パディングを使う

パディングを使うと、pで指定した桁数に満たない場合は空白で埋められます。

    @Test
    void paddingShortFormat() {
        DateTimeFormatter dateFormatter =
                DateTimeFormatter.ofPattern("uuuu/ppM/ppd").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDate.of(2022, 10, 25).format(dateFormatter))
                .isEqualTo("2022/10/25");
        assertThat(LocalDate.of(2023, 1, 5).format(dateFormatter))
                .isEqualTo("2023/ 1/ 5");

        DateTimeFormatter dateTimeFormatter =
                DateTimeFormatter.ofPattern("uuuu/ppM/ppd ppH:ppm:pps").withResolverStyle(ResolverStyle.STRICT);
        assertThat(LocalDateTime.of(2022, 10, 25, 16, 30, 25).format(dateTimeFormatter))
                .isEqualTo("2022/10/25 16:30:25");
        assertThat(LocalDateTime.of(2023, 1, 5, 2, 3, 4).format(dateTimeFormatter))
                .isEqualTo("2023/ 1/ 5  2: 3: 4");
    }

こんなところでしょうか。

まとめ

いかに雰囲気でDateTimeFormatterを使っていたのかが、よくわかりました…。

書式文字列のチェックでDateTimeFormatterを使う時は、他の確認方法と組み合わせることも考えた方がいいのかもしれません。
また、パースとフォーマットで同一のパターン定義で良いのかも考えどころですね。

あとは、こちらを使うなどでしょうか。

DateTimeFormatter#parse(java.lang.CharSequence,java.text.ParsePosition))