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 へのマッピングが載っています。
パターン文字 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