This is a continuation of http://www.clevercaboose.com/2026/07/15/java-datatimeformatter-appendoptional-issue/
No one would recommend a serious parser that just iterated through an exhaustive list of possibilities. So I continued this obsessive, possessed, labor-of-love a little further. This is a hard problem.
Two big issues with DateTimeFormatter:
- It is greedy.
- There is no way to specify “one of many”
You can work around “greedy”. You cannot work around “one of many”. All you can do is provide “zero or more of many” and hope that greedy catches failures.
Also, this is an unsolvable problem in the sense that the solution will often be ambiguous. You’d never use this for “backend work”, obviously. And in the UI, you are relying on the user to check that the interpreted value is correct. That is unacceptable in some circles.
It might be acceptable to provide 3 results:
- Correct value parsed with confidence. (Green)
- A value was parsed but it might be wrong. (Orange)
- A value could not be parsed. (Error/Red)
So I soldiered on. Creating a sequential list of optional patterns comes very close to working, but not quite. I ran into an obscure error where enough “[-]” patterns in sequence ate up the optional spaces and swallowed the “-” of the time zone offset “-05:30”.
Search google and you’ll see a suggestions to “nest the optionals” rather than just apply them sequentially. But that only works when you have a required constraint at the end of the chain. The problem for us is that our chain is all optional, so nested optionals have the same outcome as flattened optionals.
In the end, I solved the special cases by appending specific patterns to handle these errors. Ugly but necessary.
This is the best I have come up with so far. First a Builder to wrap a Builder:
package com.mri.util.time;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalField;
import java.util.Locale;
/**
* Criticism of DateTimeFormatterBuilder:
* 1) No way to declare oneOfMany.
* 2) Everything is greedy
*/
public class DateTimeFormatterBuilderBuilder {
final DateTimeFormatterBuilder builder;
final Locale locale;
public DateTimeFormatterBuilderBuilder() {
this(Locale.ENGLISH);
}
public DateTimeFormatterBuilderBuilder(Locale locale) {
this.locale = locale;
builder = new DateTimeFormatterBuilder().parseCaseInsensitive().parseStrict();
}
public DateTimeFormatter toFormatter() {
return builder.toFormatter(locale);
}
// #region separator
// This is still optional. No way to make this a requirement
// Make the space the last check. Otherwise will swallow " -", which catches the zone offsets
static final String separatorPattern = "[:][/][-][,][.][ ]";
static final DateTimeFormatter separatorOptional = DateTimeFormatter.ofPattern(separatorPattern);
public DateTimeFormatterBuilderBuilder appendOptionalSeparator() {
builder.appendOptional(separatorOptional); // could have also done builder.append(separatorOptional);
return this;
}
// #endregion
// #region optional descending from month to nano
/**
* year4 is required. everything following is optional
*/
public DateTimeFormatterBuilderBuilder year4MonthDayHourMinSecNanoTzOptional() {
year4();
appendOptionalSeparator();
monthDayHourMinSecNanoTzOptional();
defaultingAll();
return this;
}
/**
* year2 is required. everything following is optional
*/
public DateTimeFormatterBuilderBuilder year2SpaceMonthDayHourMinSecNanoTzOptional() {
year2();
appendOptionalSeparator();
monthDayHourMinSecNanoTzOptional();
defaultingAll();
return this;
}
public DateTimeFormatterBuilderBuilder year4() {
builder.appendValue(ChronoField.YEAR, 4); // this is exact
// builder.appendPattern("yyyy"); //this consumes 2 to 19 characters and is more flexible but can cause problems with neighbors
return this;
}
public DateTimeFormatterBuilderBuilder year2() {
builder.appendValueReduced(ChronoField.YEAR, 2, 2, LocalDate.of(1980, 1, 1)); // valid values from {@code 1980} to {@code 2079}
// builder.appendPattern("yy");
return this;
}
/**
* Still have an issue in this method. Turns out that the greedy year has fewer false parsing errors
* than specific widths. I don't know why and haven't investigated.
*/
public DateTimeFormatterBuilderBuilder yearAll() {
if (true) {
builder.appendPattern("[yyyy][yy]");
} else {
// This has a higher number of erroneous parsings
DateTimeFormatter year2 = new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR, 4).toFormatter();
DateTimeFormatter year1 = new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR, 2).toFormatter();
appendOneOf(year2, year1);
}
return this;
}
public DateTimeFormatterBuilderBuilder month2() {
builder.appendValue(ChronoField.MONTH_OF_YEAR, 2);
return this;
}
public DateTimeFormatterBuilderBuilder monthAll() {
appendPattern("[MMMM][MMM]"); // ChronoField not used for text inputs
DateTimeFormatter month2 = new DateTimeFormatterBuilder().appendValue(ChronoField.MONTH_OF_YEAR, 2).toFormatter();
DateTimeFormatter month1 = new DateTimeFormatterBuilder().appendValue(ChronoField.MONTH_OF_YEAR, 1).toFormatter();
return appendOneOf(month2, month1);
}
public DateTimeFormatterBuilderBuilder dayAll() {
DateTimeFormatter day2 = new DateTimeFormatterBuilder().appendValue(ChronoField.DAY_OF_MONTH, 2).toFormatter();
DateTimeFormatter day1 = new DateTimeFormatterBuilder().appendValue(ChronoField.DAY_OF_MONTH, 1).toFormatter();
return appendOneOf(day2, day1);
}
public DateTimeFormatterBuilderBuilder day2() {
builder.appendValue(ChronoField.DAY_OF_MONTH, 2);
return this;
}
public DateTimeFormatterBuilderBuilder hour2() {
builder.appendValue(ChronoField.HOUR_OF_DAY, 2);
return this;
}
public DateTimeFormatterBuilderBuilder minute2() {
builder.appendValue(ChronoField.MINUTE_OF_HOUR, 2);
return this;
}
public DateTimeFormatterBuilderBuilder second2() {
builder.appendValue(ChronoField.SECOND_OF_MINUTE, 2);
return this;
}
public DateTimeFormatterBuilderBuilder hourAll() {
DateTimeFormatter hour2 = new DateTimeFormatterBuilder().appendValue(ChronoField.HOUR_OF_DAY, 2).toFormatter();
DateTimeFormatter hour1 = new DateTimeFormatterBuilder().appendValue(ChronoField.HOUR_OF_DAY, 1).toFormatter();
return appendOneOf(hour2, hour1);
}
public DateTimeFormatterBuilderBuilder minuteAll() {
DateTimeFormatter minute2 = new DateTimeFormatterBuilder().appendValue(ChronoField.MINUTE_OF_HOUR, 2).toFormatter();
DateTimeFormatter minute1 = new DateTimeFormatterBuilder().appendValue(ChronoField.MINUTE_OF_HOUR, 1).toFormatter();
return appendOneOf(minute2, minute1);
}
public DateTimeFormatterBuilderBuilder secondAll() {
// this fails in the "MM dd yyy hh mm n tz" case ("05 12 1963 01 01 -05:30")
// problem is that greedy parsing in the optional separator swallows the space here, and the subsequent nano parsing
// swallows the minus sign of the time zone offset. And then the timezone parser fails because 05:30 is illegal (sign is required)
optionalStart();
DateTimeFormatter second2 = new DateTimeFormatterBuilderBuilder().appendValue(ChronoField.SECOND_OF_MINUTE, 2).toFormatter();
DateTimeFormatter second1 = new DateTimeFormatterBuilderBuilder().appendValue(ChronoField.SECOND_OF_MINUTE, 1).toFormatter();
appendOneOf(second2, second1);
optionalEnd();
return this;
}
public DateTimeFormatterBuilderBuilder secondAllX() {
optionalStart();
// moving the optional separator inside the required element so the position rolls back if not parsed.
// This eliminates the parsing errors (Exception because failed to parse) but greatly increases the parsed errors
// (parsed but incorrect value), because now the timezone offsets are parsed as nanoseconds.
DateTimeFormatter second2 = new DateTimeFormatterBuilderBuilder().appendOptionalSeparator().appendValue(ChronoField.SECOND_OF_MINUTE, 2).toFormatter();
DateTimeFormatter second1 = new DateTimeFormatterBuilderBuilder().appendOptionalSeparator().appendValue(ChronoField.SECOND_OF_MINUTE, 1).toFormatter();
appendOneOf(second2, second1);
optionalEnd();
return this;
}
/**
* The month-day-year version, with everything else being optional.
* If we use MM for the month, then there could be ambiguity with year-month-day.
* Could solve it by forcing a space after month. If so, consistency suggests spaces between month, day, and year, and hour.
* Or could just require a space after year. That is still a unique quirk, but is less visible/likely-problem
*/
public DateTimeFormatterBuilderBuilder monthDayYearHourMinSecNanoTzOptional() {
monthAll();
appendOptionalSeparator();
dayAll();
appendOptionalSeparator();
yearAll();
appendOptionalSeparator(); // to differentiate between year-month-day
hourMinSecNanoTzOptional();
defaultingAll();
return this;
}
/**
* The monthDayYearHourMinSecNanoTzOptional has one failure case:
* If there are no seconds, then it swallows the separator. The subsequent nano
* parser swallows the next separator, which includes the "-" of a ZoneOffset (e.g., -05:30).
* So the ZoneOffset parser fails on 05:30 because there is no sign.
* No known solution. So just add another backup case that skips the seconds and nanos
*
* BY THE WAY. THIS SHOWS ANOTHER ISSUE: IF THE USER SKIPS SECONDS BUT INCLUDES NANOSECONDS, WHO KNOWS WHAT WILL HAPPEN?
*/
public DateTimeFormatterBuilderBuilder monthDayYearHourMinTzOptional() {
monthAll();
appendOptionalSeparator();
dayAll();
appendOptionalSeparator();
yearAll();
appendOptionalSeparator(); // to differentiate between year-month-day
hourMinTzOptional();
defaultingAll();
return this;
}
public DateTimeFormatterBuilderBuilder year4MonthDayHourMinTzOptional() {
year4();
appendOptionalSeparator();
monthDayHourMinTzOptional();
defaultingAll();
return this;
}
private DateTimeFormatterBuilderBuilder monthDayHourMinSecNanoTzOptional() {
monthAll();
appendOptionalSeparator();
dayHourMinSecNanoTzOptional();
return this;
}
private DateTimeFormatterBuilderBuilder monthDayHourMinTzOptional() {
monthAll();
appendOptionalSeparator();
dayHourMinTzOptional();
return this;
}
private DateTimeFormatterBuilderBuilder dayHourMinSecNanoTzOptional() {
dayAll();
appendOptionalSeparator();
hourMinSecNanoTzOptional();
return this;
}
private DateTimeFormatterBuilderBuilder dayHourMinTzOptional() {
dayAll();
appendOptionalSeparator();
hourMinTzOptional();
return this;
}
private DateTimeFormatterBuilderBuilder hourMinSecNanoTzOptional() {
hourAll();
appendOptionalSeparator();
minuteAll();
appendOptionalSeparator();
secondAll();
appendOptionalSeparator();
appendPattern("[n]");
appendAllTimeZones();
return this;
}
private DateTimeFormatterBuilderBuilder hourMinTzOptional() {
hourAll();
appendOptionalSeparator();
minuteAll();
appendAllTimeZones();
return this;
}
public DateTimeFormatterBuilderBuilder yyyymmddOptional() {
year4();
month2();
day2();
appendOptionalSeparator();
hourMinSecNanoTzOptional();
return this;
}
public DateTimeFormatterBuilderBuilder mmddyyyyOptional() {
month2();
day2();
year4();
appendOptionalSeparator();
hourMinSecNanoTzOptional();
return this;
}
// #endregion
// #region timezones
// Need a separator before the timezone. Cannot include '-' because that swallows the "minus". Just use a space
// Make it optional because everything in this series is optional
private static final String allTimeZonesWithSpacePrefixStr = "[ ][VV][xxxx][XXX][z][zzzz]";
static final DateTimeFormatter allTimeZonesWithSpacePrefixEnglish = DateTimeFormatter.ofPattern(allTimeZonesWithSpacePrefixStr, Locale.ENGLISH);
// Tried to improve the previous line but could not. This produces worse results. Still investigating...
// private static final DateTimeFormatter allTimeZonesWithSpacePrefix = allTimeZonesInferior(Locale.ENGLISH);
// static DateTimeFormatter allTimeZonesInferior(Locale locale) {
// DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
// builder.appendPattern("[ ]");
// builder.optionalStart().appendZoneOrOffsetId().optionalEnd();
// builder.optionalStart().appendLocalizedOffset(TextStyle.FULL).optionalEnd();
// builder.optionalStart().appendLocalizedOffset(TextStyle.SHORT).optionalEnd();
// for (TextStyle textStyle : TextStyle.values()) {
// builder.optionalStart().appendZoneText(textStyle).optionalEnd();
// }
// return builder.toFormatter(locale);
// }
/**
* Include one of all four timezone formats
*
* Symbol Meaning Example
* VV Full Time Zone ID America/Chicago, Asia/Tokyo
* z Short Zone Name CST, PST, GMT
* Z Offset without colons +0530, -0800
* XXX Offset with colons +05:30, -08:00
*
* VV (Zone ID): Parses regional IDs like Europe/Paris or America/New_York.
* x (Zone Offset): Parses formats like +05 or +0530 (outputs +00 for zero).
* XX (Zone Offset): Parses formats like +0530 or Z (outputs Z for zero).
* XXX (Zone Offset with Colon): Parses standard formats like +05:30 or Z.
* z (Short Zone Name): Parses abbreviations like EST, PST, or GMT
* zzzz (Long Zone Name): Parses full names like Pacific Standard Time
*/
public DateTimeFormatterBuilderBuilder appendAllTimeZones() {
// requiredSpace(); //Cannot be a separator because it swallows the "minus"
// used cache value when possible
if (locale == Locale.ENGLISH) {
return append(allTimeZonesWithSpacePrefixEnglish);
} else {
return append(DateTimeFormatter.ofPattern(allTimeZonesWithSpacePrefixStr, locale));
}
}
// #endregion
public DateTimeFormatterBuilderBuilder defaultingAll() {
builder.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1);
builder.parseDefaulting(ChronoField.DAY_OF_MONTH, 1);
builder.parseDefaulting(ChronoField.HOUR_OF_DAY, 0);
builder.parseDefaulting(ChronoField.MINUTE_OF_HOUR, 0);
builder.parseDefaulting(ChronoField.SECOND_OF_MINUTE, 0);
builder.parseDefaulting(ChronoField.NANO_OF_SECOND, 0);
return this;
}
public DateTimeFormatterBuilderBuilder requiredSpace() {
builder.appendLiteral(' ');
return this;
}
public DateTimeFormatterBuilderBuilder append(DateTimeFormatter formatter) {
builder.append(formatter);
return this;
}
private DateTimeFormatterBuilderBuilder optionalStart() {
builder.optionalStart();
return this;
}
private DateTimeFormatterBuilderBuilder optionalEnd() {
builder.optionalEnd();
return this;
}
public DateTimeFormatterBuilderBuilder appendOptional(DateTimeFormatter formatter) {
builder.appendOptional(formatter);
return this;
}
public DateTimeFormatterBuilderBuilder appendPattern(String str) {
builder.appendPattern(str);
return this;
}
public DateTimeFormatterBuilderBuilder appendOptionalPatterns(String... patterns) {
optionalStart();
for (String pattern : patterns) {
optionalStart();
appendPattern(pattern);
optionalEnd();
}
optionalEnd();
return this;
}
public DateTimeFormatterBuilderBuilder appendLiteral(String str) {
builder.appendLiteral(str);
return this;
}
public DateTimeFormatterBuilderBuilder appendValue(TemporalField field, int width) {
builder.appendValue(field, width);
return this;
}
// warning: there is no way to specify that one of them MUST be required
// just append them all sequentially as optional
public DateTimeFormatterBuilderBuilder appendOneOf(DateTimeFormatter... formatters) {
for (DateTimeFormatter formatter : formatters) {
appendOptional(formatter);
}
return this;
}
}
And obligatory global utility classes:
public final class MriUtilsLocalDate {
package com.mri.util.time;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
//package visible
final class MriUtilsLocalDate {
public static final DateTimeFormatter ISO_DATE_FORMATTER = DateTimeFormatter.ISO_DATE; // ISO-8601
public static final String ISO_DATE = "yyyy-MM-dd"; // corresponds ISO_DATE_FORMATTER
static {
MriUtilsTime.formatterMap.put(ISO_DATE, ISO_DATE_FORMATTER);
}
// make sure this comes AFTER the statics
public static final MriUtilsLocalDate INSTANCE = new MriUtilsLocalDate();
/**
* A list of DateTimeFormatters that are used in sequence to attempt to parse a String as a LocalDate
*/
final List
<DateTimeFormatter> sequencedFormatters;
private MriUtilsLocalDate() {
sequencedFormatters = new ArrayList<>();
sequencedFormatters.add(ISO_DATE_FORMATTER);
sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().yyyymmddOptional().toFormatter());
sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().mmddyyyyOptional().toFormatter());
sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().year4MonthDayHourMinSecNanoTzOptional().toFormatter());
sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().year2SpaceMonthDayHourMinSecNanoTzOptional().toFormatter());
sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().monthDayYearHourMinSecNanoTzOptional().toFormatter());
//all these other cases to catch perverse issues with minus signs in ZoneOffset and the greedy "-" separator
sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().monthDayYearHourMinTzOptional().toFormatter());
sequencedFormatters.add(new DateTimeFormatterBuilderBuilder().year4MonthDayHourMinTzOptional().toFormatter());
}
/**
* Expect this to be typed in by a user who will inspect the results.
* So do our best to find a result, but there is a possibility it will be wrong
*/
LocalDate parseFlexibleLocalDate(CharSequence text) {
return MriUtilsTimePriv.parseFormatterSequence(sequencedFormatters, text, LocalDate::from);
}
}
package com.mri.util.time;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
//package visible
final class MriUtilsLocalDateTime {
public static final DateTimeFormatter ISO_LOCAL_DATE_TIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
public static final String ISO_LOCAL_DATE_TIME = "yyyy-MM-dd'T'HH:mm:ss";
static {
MriUtilsTime.formatterMap.put(ISO_LOCAL_DATE_TIME, ISO_LOCAL_DATE_TIME_FORMATTER);
}
// this no longer works because the yyyy is greedy. Have to use ChronoField4 instead
// public static final String YYYYMMDDHH = MriUtilsLocalDate.YYYYMMDD+"HH";
// make sure this comes AFTER the statics
public static final MriUtilsLocalDateTime INSTANCE = new MriUtilsLocalDateTime();
/**
* A large list of DateTimeFormatters that are used in sequence to attempt to parse a String as a LocalDate
*/
final List
<DateTimeFormatter> sequencedFormatters;
private MriUtilsLocalDateTime() {
sequencedFormatters = new ArrayList<>();
sequencedFormatters.add(ISO_LOCAL_DATE_TIME_FORMATTER);
sequencedFormatters.addAll(MriUtilsLocalDate.INSTANCE.sequencedFormatters);
}
/**
* Expect this to be typed in by a user who will inspect the results.
* So do our best to find a result, but there is a possibility it will be wrong
*/
LocalDateTime parseFlexibleLocalDateTime(CharSequence text) {
return MriUtilsTimePriv.parseFormatterSequence(sequencedFormatters, text, LocalDateTime::from);
}
}
package com.mri.util.time;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;
import org.jspecify.annotations.Nullable;
//package visible
final class MriUtilsInstant {
// ISO-8601 '2011-12-03T10:15:30Z'
public static final DateTimeFormatter ISO_INSTANT = DateTimeFormatter.ISO_INSTANT;
// Standard iso format for zoneddatetime/instant parsing. user includes timezone
public static final String MM_DD_YYYY_HH_MM_SS_NANO_ZONE = "MM/dd/yyyy HH:mm:ss.n'['VV']'";
public static final DateTimeFormatter MM_DD_YYYY_HH_MM_SS_NANO_ZONE_FORMATTER = new DateTimeFormatterBuilder()
.appendPattern("MM/dd/yyyy HH:mm:ss")
.appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
.appendLiteral(' ')
.appendZoneId()
.toFormatter();
// make sure this goes after the statics
public static final MriUtilsInstant INSTANCE = new MriUtilsInstant();
/**
* A large list of DateTimeFormatters that are used in sequence to attempt to parse a String as a LocalDate
*/
private final List
<DateTimeFormatter> sequencedFormatters;
private MriUtilsInstant() {
sequencedFormatters = new ArrayList<>();
sequencedFormatters.add(ISO_INSTANT);
sequencedFormatters.add(MM_DD_YYYY_HH_MM_SS_NANO_ZONE_FORMATTER);
sequencedFormatters.addAll(MriUtilsLocalDateTime.INSTANCE.sequencedFormatters);
}
/**
* Expect this to be typed in by a user who will inspect the results.
* So do our best to find a result, but there is a possibility it will be wrong.
* Not all formats require a TimeZone, as sometimes the TZ will be in the String.
*/
public Instant parseFlexibleInstant(CharSequence text, @Nullable TimeZone defaultTimeZone) {
TemporalAccessor temporal = MriUtilsTimePriv.parseFormatterSequenceBest(sequencedFormatters, text);
return MriUtilsTime.instantFromTemporal(temporal, defaultTimeZone);
}
}
public static @Nullable Instant instantFromTemporal(@Nullable TemporalAccessor temporal, @Nullable TimeZone defaultTimeZone) {
return switch (temporal) {
case Instant inst -> inst;
case ZonedDateTime zoned -> zoned.toInstant();
case LocalDateTime local -> localDateTimeToInstant(local, defaultTimeZone);
case LocalDate localDate -> localDateTimeToInstant(localDate.atStartOfDay(), defaultTimeZone);
case LocalTime localTime -> localDateTimeToInstant(localTime.atDate(LocalDate.now()), defaultTimeZone);
case null -> null;
default -> throw new IllegalArgumentException("Unexpected class= " + temporal.getClass() + " value: " + temporal);
};
}
public static @Nullable Instant localDateTimeToInstant(@Nullable LocalDateTime localDateTime, @Nullable TimeZone tz) {
if (localDateTime == null || tz == null) {
return null;
}
return localDateTime.atZone(tz.toZoneId()).toInstant();
}
}
class MriUtilsTimePriv {
private static Logger LOG = org.apache.logging.log4j.LogManager.getLogger(MriUtilsTimePriv.class);
public static TemporalAccessor parseFormatterSequenceBest(List
<DateTimeFormatter> formatters, CharSequence text, TemporalQuery<?>... queries) {
for (DateTimeFormatter formatter : formatters) {
try {
return formatter.parseBest(text, queries);
} catch (DateTimeParseException ignore) {
// noop
LOG.trace("ignore: text={} formatter={}", text, formatter, ignore);
} catch (Exception e) {
LOG.warn("Ignoring exception text={} formatter={} ex={}", text, formatter, e);
}
}
throw MriException.from("Could not parse temporal = " + text);
}
public static TemporalAccessor parseFormatterSequenceBest(List
<DateTimeFormatter> formatters, CharSequence text) {
return parseFormatterSequenceBest(formatters, text, Instant::from, LocalDateTime::from, LocalDate::from, LocalTime::from);
}
public static
<T> T parseFormatterSequence(List<DateTimeFormatter> formatters, CharSequence text, TemporalQuery<T> query) {
for (DateTimeFormatter formatter : formatters) {
try {
return formatter.parse(text, query);
} catch (DateTimeParseException ignore) {
// noop
LOG.trace("ignore: text={} formatter={}", text, formatter, ignore);
} catch (Exception e) {
LOG.warn("Ignoring exception text={} formatter={}", text, formatter);
}
}
throw MriException.from("Could not parse temporal = " + text);
}
}
I’m not going to show the JUnit test classes because of the amount of code. (And this doesn’t dseserve its own github repository.) However, I am pleased with the results so far. Currently with a few thousand tests (can easily add many thousands more), no parse failures, and the only errors are caused by greedy/ambiguous parsing, which I don’t think I can fix.


