Debashish Mitra
Debashish Mitra

Reputation: 61

Java 8 DateTimeFormatterBuilder().appendOptional not working

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

  1. What is going wrong here ? Why is it not working for "10/30/2017" ?

  2. 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)

Upvotes: 6

Views: 3445

Answers (2)

erickson
erickson

Reputation: 269807

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 */

Upvotes: 2

Kevin Wang
Kevin Wang

Reputation: 182

The formatter does not work the way you expect, the optional part means

  • if there is nothing extra attached to the first pattern (e.g., "MM/dd/yy"), that is fine,
  • if there is something extra, it needs to match the second pattern (e.g, "MM/dd/yyyy")

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 check the string by length or regex first before doing the date string parsing
  • 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;
    });
    

Upvotes: 5

Related Questions