I wrote this code to parse dates from the output of the OCR, which means that the obtained date can be literally anything, so I put some restrictions in place:
- Date is the the format of:
field1?field2?field3
, where the fields are either day, month or year and any delimiter can be used to split the numbers, this is also called the short format of dates.
- The fields consist of only numbers. (So no months as text)
- The locale is known.
I first played around with DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(locale)
, but it turned out to be only of use for formatting, and not for parsing, as it only gives one specific format per locale.
So I decided to roll out my own code whilst still intending to use as many Java library features as possible (mainly from java.util.Locale
and java.time
).
The test class:
public class DateParserTest {
@Test
public void testParseDutchDate() {
List<String> dates = Arrays.asList(
"02-10-2014",
"2-10-2014",
"02-10-14",
"2-10-14",
"02/10/2014",
"02 10 2014"
);
for (String date : dates) {
Locale locale = new Locale("nl");
LocalDate localDate = DateParser.parseShortDate(date, locale);
assertEquals(LocalDate.of(2014, 10, 2), localDate);
}
}
@Test
public void testParseAmericanDate() {
List<String> dates = Arrays.asList(
"10-02-2014",
"10-2-2014",
"10-02-14",
"10-2-14",
"10/02/2014",
"10 02 2014"
);
for (String date : dates) {
FormatStyle formatStyle = FormatStyle.SHORT;
Locale locale = new Locale("en-US");
LocalDate localDate = DateParser.parseShortDate(date, locale);
assertEquals(LocalDate.of(2014, 10, 2), localDate);
}
}
}
The parser class:
public final class DateParser {
private DateParser() {
throw new UnsupportedOperationException();
}
private final static Pattern PARSE_DATE_PATTERN = Pattern.compile("(?iuU)^\\W*([\\w]+)\\W+([\\w]+)\\W+([\\w]+)\\W*$");
private static final Pattern DATE_PATTERN_EXTRACTION_PATTERN = Pattern.compile("^(\\w+)\\W+(\\w+)\\W+(\\w+)$");
public static LocalDate parseShortDate(final String text, final Locale locale) {
Objects.requireNonNull(text, "text");
Objects.requireNonNull(locale, "locale");
Matcher matcher = PARSE_DATE_PATTERN.matcher(text);
if (!matcher.matches()) {
throw new IllegalStateException("Date " + text + " for locale " + locale + " could not be matched");
}
String match1 = matcher.group(1);
String match2 = matcher.group(2);
String match3 = matcher.group(3);
String pattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.SHORT, null, Chronology.ofLocale(locale), locale);
System.out.println("pattern = " + pattern);
Matcher datePatternMatcher = DATE_PATTERN_EXTRACTION_PATTERN.matcher(pattern);
if (!datePatternMatcher.matches()) {
throw new IllegalStateException("Date format " + pattern + " for locale " + locale + " could not be processed");
}
String datePatternMatch1 = datePatternMatcher.group(1);
String datePatternMatch2 = datePatternMatcher.group(2);
String datePatternMatch3 = datePatternMatcher.group(3);
String resolvedPattern = new StringBuilder()
.append(String.join("", Collections.nCopies(match1.length(), datePatternMatch1.substring(0, 1))))
.append('-')
.append(String.join("", Collections.nCopies(match2.length(), datePatternMatch2.substring(0, 1))))
.append('-')
.append(String.join("", Collections.nCopies(match3.length(), datePatternMatch3.substring(0, 1))))
.toString();
return LocalDate.parse(String.join("-", match1, match2, match3), DateTimeFormatter.ofPattern(resolvedPattern, locale));
}
}
I'd like to have a general review with extra focus on how to be able to support as many input variations and locales as possible.
This type of problem also has some practical issues you will only find out when practicing by solving this problem, so I strongly encourage you to review this code with an IDE at hand to be able to test alternative suggestions.