My requirement is to validate that a date String is in the correct format based on a set of valid formats specified.
Valid formats:
MM/dd/yy
MM/dd/yyyy
I created a simple test method that uses the Java 8 DateTimeFormatterBuilder to create a flexible formatter that supports multiple optional formats. Here is the code:
public static void test() {
    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendOptional(DateTimeFormatter.ofPattern("MM/dd/yy"))
            .appendOptional(DateTimeFormatter.ofPattern("MM/dd/yyyy"))
            .toFormatter();
    String dateString = "10/30/2017";
    try {
        LocalDate.parse(dateString, formatter);
        System.out.println(dateString + " has a valid date format");
    } catch (Exception e) {
        System.out.println(dateString + " has an invalid date format");
    }
}
When I run this, here is the output
10/30/2017 has an invalid date format
As you see in the code, the valid date formats are MM/dd/yy and MM/dd/yyyy. My expectation was that the date 10/30/2017 should be valid as it matches MM/dd/yyyy. However, 10/30/2017 is being reported as invalid.
What is going wrong ? Why is this not working ?
I also tried
.appendOptional(DateTimeFormatter.ofPattern("MM/dd/yy[yy]"))
in place of
.appendOptional(DateTimeFormatter.ofPattern("MM/dd/yy"))
.appendOptional(DateTimeFormatter.ofPattern("MM/dd/yyyy"))
but still had the same issue.
This code runs as expected if I use:
String dateString = "10/30/17";
in place of
String dateString = "10/30/2017";
I have 2 questions
What is going wrong here ? Why is it not working for "10/30/2017" ?
Using Java 8, how to correctly create a flexible Date formatter (a formatter that supports multiple optional formats) ? I know the use of [] to create optional sections in the pattern string itself. I'm looking for something more similar to what I am trying (avoiding [] inside the pattern string and using separate optional clauses for each separate format string)
The formatter does not work the way you expect, the optional part means
To make it a bit clearer, try to run the sample code below to understand it better:
    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendOptional(DateTimeFormatter.ofPattern("MM/dd/yy"))
            .appendOptional(DateTimeFormatter.ofPattern("MM/dd/yyyy"))
            .toFormatter();
    String[] dateStrings = {
            "10/30/17",           // valid
            "10/30/2017",         // invalid
            "10/30/1710/30/2017", // valid
            "10/30/201710/30/17"  // invalid
    };
    for (String dateString : dateStrings) {
        try {
            LocalDate.parse(dateString, formatter);
            System.out.println(dateString + " has a valid date format");
        } catch (Exception e) {
            System.err.println(dateString + " has an invalid date format");
        }
    }
==
10/30/17 has a valid date format
10/30/1710/30/2017 has a valid date format
10/30/2017 has an invalid date format
10/30/201710/30/17 has an invalid date format
==
This is only a simple solution, if performance is of your concern, the validation by catching the parsing exception should be the last resort
you may also replace the stream with a method containing a simple for loop, etc.
String[] patterns = { "MM/dd/yy", "MM/dd/yyyy" };
Map<String, DateTimeFormatter> formatters = Stream.of(patterns).collect(Collectors.toMap(
        pattern -> pattern, 
        pattern -> new DateTimeFormatterBuilder().appendOptional(DateTimeFormatter.ofPattern(pattern)).toFormatter()
));
String dateString = "10/30/17";
boolean valid = formatters.entrySet().stream().anyMatch(entry -> {
    // relying on catching parsing exception will have serious expense on performance
    // a simple check will already improve a lot 
    if (dateString.length() == entry.getKey().length()) {
        try {
            LocalDate.parse(dateString, entry.getValue());
            return true;
        }
        catch (DateTimeParseException e) {
            // ignore or log it   
        }
    }
    return false;
});
The builder's appendValueReduced() method was designed to handle this case. 
When parsing a complete value for a field, the formatter will treat it as an absolute value.
When parsing an partial value for a field, the formatter will interpret it relative to a base that you specify. For example, if you want two-digit years to be interpreted as being between 1970 and 2069, you can specify 1970 as your base. Here's an illustration:
    LocalDate century = LocalDate.ofEpochDay(0); /* Beginning Jan. 1, 1970 */
    DateTimeFormatter f = new DateTimeFormatterBuilder()
            .append(DateTimeFormatter.ofPattern("MM/dd/"))
            .appendValueReduced(ChronoField.YEAR, 2, 4, century)
            .toFormatter();
    System.out.println(LocalDate.parse("10/30/2017", f)); /* 2017-10-30 */
    System.out.println(LocalDate.parse("10/30/17", f));   /* 2017-10-30 */
    System.out.println(LocalDate.parse("12/28/1969", f)); /* 1969-12-28 */
    System.out.println(LocalDate.parse("12/28/69", f));   /* 2069-12-28 */
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With