Java DataTimeFormatter appendOptional issue

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:

JUnit results

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/

Leave a Reply

Your email address will not be published. Required fields are marked *