🗓️

java.time.format.DateTimeParseException の対処法

に公開

環境

Open JDK 23.0.1

Unable to obtain {Temporal} from TemporalAccessor

エラーが発生するコード例

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年M月d日[ H:mm]");

LocalDateTime withTime = LocalDateTime.parse("2025年1月2日 03:45", formatter);
// ↓ DateTimeParseExceptionが発生する
LocalDateTime withoutTime = LocalDateTime.parse("2025年1月2日", formatter);

エラー内容

java.time.format.DateTimeParseException: Text '2025年1月2日' could not be parsed: Unable to obtain LocalDateTime from TemporalAccessor: {},ISO resolved to 2025-01-02 of type java.time.format.Parsed
	at java.base/java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:2079)
	at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:2014)
	at java.base/java.time.LocalDateTime.parse(LocalDateTime.java:494)

原因

1. LocalDateTime に必要な時刻情報が欠けている

LocalDateTime.parse() でパースする際、内部で LocalDateTime.from() が呼び出されて TemporalAccessor から LocalDateTime に変換しようとします。
このとき、日付と時刻の両方が含まれていないと変換できず、DateTimeParseException が発生します。

上の例では "2025年1月2日" に時刻情報が含まれないために同エラーが発生しています。

2. パターン文字列と日付文字列がマッチしていない

パターンと日付文字列がマッチしない場合にも、同じ Unable to obtain {Temporal} from TemporalAccessor が発生します。

例)DateTimeFormatter.ofPattern("yyyy/MM/dd") に対して "2025年1月2日" という文字列をパースすると、パターンと文字列が合わずに DateTimeParseException が発生します。

解決方法

1. LocalDateTime に必要な時刻情報が欠けている

パターンにオプションが含まれていて複数の型でパースできる場合は、DateTimeFormatter.parseBest() を使いましょう。

下記の例では、時刻情報が存在すれば LocalDateTime、なければ LocalDate としてパースするようにしています。

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy年M月d日[ H:mm]");

// parseBest() は先に LocalDateTime::from を試し、
// 失敗したら次の LocalDate::from を試す
TemporalAccessor temporalAccessor = formatter
    .parseBest("2025年1月2日", LocalDateTime::from, LocalDate::from);

if (temporalAccessor instanceof LocalDateTime) {
    // LocalDateTimeとして扱う
    LocalDateTime withoutTime = (LocalDateTime) temporalAccessor;
} else {
    // LocalDateとして扱い、時刻を0:00に設定
    LocalDateTime withoutTime = ((LocalDate) temporalAccessor).atStartOfDay();
}

2. パターンと日付文字列がマッチしていない

パターンと日付文字列を揃え、不正な日付文字列は DateTimeParseException をキャッチしてエラーハンドリングするようにします。

Conflict found: {TemporalField} {old value} differs from {TemporalField} {new value}

エラーが発生するコード例

int currentYear = Year.now().getValue();
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder()
    .appendPattern("[yyyy年]M月d日")
    .parseDefaulting(ChronoField.YEAR, currentYear);
DateTimeFormatter formatter = builder.toFormatter();

LocalDate withoutYear = LocalDate.parse("1月1日", formatter);
// ↓ DateTimeParseExceptionが発生する
LocalDate withYear = LocalDate.parse("2030年1月1日", formatter);

エラー内容

java.time.format.DateTimeParseException: Text '2030年1月1日' could not be parsed: Conflict found: Year 2024 differs from Year 2030
	at java.base/java.time.format.DateTimeFormatter.createError(DateTimeFormatter.java:2079)
	at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:2014)
	at java.base/java.time.LocalDate.parse(LocalDate.java:435)
	at Main.parseOptionalYear(Main.java:34)
	at Main.main(Main.java:13)

原因

DateTimeFormatter や DateTimeFormatterBuilder で指定するパターンは、TemporalField に対応しています。
DateTimeFormatterBuilder.appendPattern() のドキュメントの下の方に、パターン文字と Builder へのマッピングが載っています。

https://docs.oracle.com/javase/jp/23/docs/api/java.base/java/time/format/DateTimeFormatterBuilder.html#appendPattern(java.lang.String)

パターン文字 y は ChronoField.YEAR_OF_ERA に対応しますが、DateTimeFormatterBuilder.getDefaulting() で ChronoField.YEAR を指定しているため、パース時に整合性が取れず DateTimeParseException が発生しています。

解決方法

DateTimeFormatterBuilder.parseDefaulting() の引数でパターン文字に応じた TemporalField を指定するようにします。

今回のエラーでは ChronoField.YEAR_OF_ERA を指定します。

int currentYear = Year.now().getValue();
DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder()
    .appendPattern("[yyyy年]M月d日")
    .parseDefaulting(ChronoField.YEAR_OF_ERA, currentYear);
DateTimeFormatter formatter = builder.toFormatter();

Discussion