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.
CodePudding user response:
DateTimeFormatterBuilder.appendValue(TemporalField, int)
I am using and slightly modifying the idea mentioned by user16320675 in a comment:
try using a
DateTimeFormatterBuilder
and useappendValue(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 perSignStyle.EXCEEDS_PAD
.