How to use DateTimeFormatter for pattern dMMyyyy



I need to parse Strings to LocalDate using DateTimeFormatter.

There are 2 different cases, Strings of pattern dMMyy or ddMMyy (20320, 020320, 120320) and Strings of pattern ddMMyyyy or dMMyyyy (2032020, 02032020, 12032020).

For the first case i can just use DateTimeFormatter.ofPattern(“dMMyy”) which works with 5 or 6 digits long dates.

 @Test
    public void dateConversionyy() {
       DateTimeFormatter worksShortd = DateTimeFormatter.ofPattern("dMMyy");
       DateTimeFormatter worksShortdd = DateTimeFormatter.ofPattern("ddMMyy");
       String longString = "120320";
       String leadingZeroString = "020320";
       String shortString = "20320";

       LocalDate res = LocalDate.parse(longString, worksShortd);
       assertNotNull(res);

       LocalDate res2 = LocalDate.parse(longString, worksShortdd);
       assertNotNull(res2);
       assertEquals(res, res2);

       LocalDate res3 = LocalDate.parse(shortString, worksShortd);
       assertNotNull(res3);

       LocalDate res4 = LocalDate.parse(leadingZeroString, worksShortd);
       assertNotNull(res4);
       assertEquals(res3, res4);

       LocalDate res5 = LocalDate.parse(leadingZeroString, worksShortdd);
       assertNotNull(res);
       assertEquals(res3, res5);

    }

For the second case i thought i could use pattern “dMMyyyy”, but it just dont parse any input String.

 @Test
    public void dateConversionyyyy() {
        LocalDate res;
       DateTimeFormatter failsLong = DateTimeFormatter.ofPattern("dMMyyyy");
       DateTimeFormatter worksLong = DateTimeFormatter.ofPattern("ddMMyyyy");
       String longString = "13082020";
       String shortString = "3082020";
       boolean notParsed1 = false;
       try {
           LocalDate.parse(longString, failsLong);
       } catch (DateTimeParseException e) {
           notParsed1 = true;
       }
       assertTrue(notParsed1);
       res = LocalDate.parse(longString, worksLong);
       assertNotNull(res);

        boolean notParsed2 = false;
        try {
            LocalDate.parse(shortString, failsLong);
        } catch (DateTimeParseException e) {
            notParsed2 = true;
        }
        assertTrue(notParsed2);

    }

Edit: My Question: Is there a way to instantiate DateTimeFormatter use DateTimeFormatter.ofPattern in a way that would parse the 7-8 digits variant?

Edited the Question so that @user16320675 answer matches, as it solved my problem.

Answer

DateTimeFormatterBuilder.appendValue(TemporalField, int)

I am using and slightly modifying the idea mentioned by user16320675 in a comment:

try using a DateTimeFormatterBuilder and use appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NEVER), use a fixed size for the other fields.

    DateTimeFormatter worksLong = new DateTimeFormatterBuilder()
            .appendPattern("dMM")
            .appendValue(ChronoField.YEAR, 4)
            .toFormatter(Locale.ROOT);
    System.out.println(LocalDate.parse("2032020", worksLong));
    System.out.println(LocalDate.parse("02032020", worksLong));
    System.out.println(LocalDate.parse("12032020", worksLong));

Output:

2020-03-02
2020-03-02
2020-03-12

If you want more consistency in building the formatter:

    DateTimeFormatter worksLong = new DateTimeFormatterBuilder()
            .appendValue(ChronoField.DAY_OF_MONTH)
            .appendValue(ChronoField.MONTH_OF_YEAR, 2)
            .appendValue(ChronoField.YEAR, 4)
            .toFormatter(Locale.ROOT);

The result is the same.

Why didn’t your pattern dMMyyyy work?

Years, or more precisely pattern letters u, y and Y, are special in DateTimeFormatter.ofPattern(). For other numeric fields four pattern letters means a field of width exactly 4. For years it means a minimum field width of 4. This in turn ruins the adjecent value parsing that worked in the two-digit year case. Instead I believe that the parser greedily uses all 7 or 8 digits for the day of month and then fails to find a month after them.

Adjacent value parsing is what made your pattern dMMyy work. It basically allows the first (and only the first) of a series of fields with no delimiters between them to have variable width when all other fields have fixed widths. So our trick to make adjacent value parsing work again is to make the year field have a fixed width of 4. The appendValue method of the builder can do this.

So how come yy worked when yyyy did not? Because years are special. yy means a fixed width of 2. yyyy means a minimum width of 4.

Documentation quote and links

From the documentation of DateTimeFormatter:

Year: The count of letters determines the minimum field width below which padding is used. If the count of letters is two, then a reduced two digit form is used. For printing, this outputs the rightmost two digits. For parsing, this will parse using the base value of 2000, resulting in a year within the range 2000 to 2099 inclusive. If the count of letters is less than four (but not two), then the sign is only output for negative years as per SignStyle.NORMAL. Otherwise, the sign is output if the pad width is exceeded, as per SignStyle.EXCEEDS_PAD.



Source: stackoverflow