I want a date/time/instant parser that “just works” for anything the user is typing in. An AI-solution that analyzed a sentence and did its best would be great, but I am not ready for that amount of work. I could write my own custom antrl parser. But really, the DataTimeFormatter should do the job.
One solution is to create a list of DataTimeFormatters and just try them one at a time. This has some inefficiency.
But DataTimeFormatter has something just for that: The .appendOptional method.
The quickest way for an expert user to type a date is without punctuation. For example, “5 12 26” (using month day year format), Or even shorter: “20260512”. I use the latter a lot because it sorts nicely.
But we also want to handle punctuation between values. A space, a comma, a dash, or a slash. It would be great if DataTimeFormatter had an appendLiteral(“[ ,-/]”) (borrowing regex notation), but it does not. It can support that, but you have to work with .optionalStart and .optionalEnd, which is a little more complicated. Still doable, though.
I have some basic cartesianProduct code, so I wrote it longhand. This is what I came up with:
/**
* A large list of DateTimeFormatters that are used in sequence to attempt to parse a String
*/
private static final List
<DateTimeFormatter> sequencedLocalDateFormatters;
public static final DateTimeFormatter TRY_ALL_LOCAL_DATE_FORMATTER;
static {
sequencedLocalDateFormatters = new ArrayList<>();
sequencedLocalDateFormatters.add(ISO_DATE_FORMATTER);
sequencedLocalDateFormatters.add(formatter(YYYYMMDD));
List
<Object> years = List.of("yyyy", "yy");
List
<Object> months = List.of("MMMM", "MMM", "MM");
List
<Object> days = List.of("dd");
List
<Object> separators = List.of(" ", "-", "/", ",");
List<List<Object>> sets = List.of(years, months, days, separators);
List<List<Object>> cartesian = cartesianProduct(sets);
for (List
<Object> list : cartesian) {
String year = (String) list.get(0);
String month = (String) list.get(1);
String day = (String) list.get(2);
String sep = (String) list.get(3);
sequencedLocalDateFormatters.add(DateTimeFormatter.ofPattern(year + sep + month + sep + day));
sequencedLocalDateFormatters.add(DateTimeFormatter.ofPattern(month + sep + day + sep + year));
}
DateTimeFormatterBuilder tryAllFormatter = new DateTimeFormatterBuilder();
for (DateTimeFormatter dateTimeFormatter : sequencedLocalDateFormatters) {
tryAllFormatter = tryAllFormatter.appendOptional(dateTimeFormatter);
}
TRY_ALL_LOCAL_DATE_FORMATTER = tryAllFormatter.toFormatter();
}
public static LocalDate parseLocalDate(CharSequence text) {
return LocalDate.parse(text, TRY_ALL_LOCAL_DATE_FORMATTER);
}
/**
* Cartesian product
* https://www.baeldung.com/java-cartesian-product-sets
*/
static
<T> List<List<Object>> cartesianProduct(List<List<Object>> sets) {
return cartesianProduct(sets,0).collect(Collectors.toList());
}
static Stream<List<Object>> cartesianProduct(List<List<Object>> sets, int index) {
if (index == sets.size()) {
List
<Object> emptyList = new ArrayList<>();
return Stream.of(emptyList);
}
List
<Object> currentSet = sets.get(index);
return currentSet.stream().flatMap(element -> cartesianProduct(sets, index+1)
.map(list -> {
List
<Object> newList = new ArrayList<>(list);
newList.add(0, element);
return newList;
}));
}
//JUnit tests
private static Stream
<Arguments> localDateData() {
return Stream.of(
Arguments.of("05 12 1963", LocalDate.of(1963, 5, 12)),
Arguments.of("05-12-1963", LocalDate.of(1963, 5, 12)),
Arguments.of("05/12/1963", LocalDate.of(1963, 5, 12)),
Arguments.of("May 12 1963", LocalDate.of(1963, 5, 12)),
Arguments.of("August 12 1963", LocalDate.of(1963, 8, 12)),
Arguments.of("05 12 63", LocalDate.of(2063, 5, 12)),
Arguments.of("05-12-63", LocalDate.of(2063, 5, 12)),
Arguments.of("05/12/63", LocalDate.of(2063, 5, 12)),
Arguments.of("May 12 63", LocalDate.of(2063, 5, 12)),
Arguments.of("August 12 63", LocalDate.of(2063, 8, 12)),
Arguments.of("19630512", LocalDate.of(1963, 5, 12)),
Arguments.of("1963-05-12", LocalDate.of(1963, 5, 12)),
Arguments.of("1963/05/12", LocalDate.of(1963, 5, 12)));
}
@ParameterizedTest
@MethodSource("localDateData")
void test1(String str, LocalDate expectedDate) {
LocalDate date = LocalDate.parse.(str, TRY_ALL_LOCAL_DATE_FORMATTER);
assertThat(date).isEqualTo(expectedDate);
}
But when I ran the unit tests, there were errors:

It should have worked. Here is the gemini suggestion for testing the case that errored.
@Test
// gemini suggestion
void geminiSuggestion() {
String dateString = "05 12 63";
// Use MM for month, dd for day, and yy for 2-digit year
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM dd yy");
LocalDate date = LocalDate.parse(dateString, formatter);
System.out.println(date); // Outputs: 2063-05-12
}
And I am certain that “MM dd yy” is in the list.
Seems like a java bug to me. If I give the DateTimeFormatter a list of DateTimeFormatters to try, it should recover from an individual error and try them all.
So I switched to the “long-hand” version:
public static LocalDate parseLocalDate(CharSequence text) {
//return LocalDate.parse(text, TRY_ALL_LOCAL_DATE_FORMATTER);
for (DateTimeFormatter dateTimeFormatter : sequencedLocalDateFormatters) {
try {
return LocalDate.parse(text, dateTimeFormatter);
} catch (Exception ignore) {}
}
throw new RuntimeException.from("Could not parse LocalDate = ", text);
}
And this time all the JUnit tests worked!
Bit of a shock. C’est la IT guerre.
Continued at http://www.clevercaboose.com/2026/07/19/more-on-javas-da…flexible-parsing/