DateTimeFormatter pattern with liternal and no separator does not work
Asked Answered
H

3

7

The parser generated by DateTimeFormatter.ofPattern exhibits the following interesting behaviour which is preventing me from writing a pattern to parse a string like 20150100:

System.out.println(DateTimeFormatter.ofPattern("yyyyMM").parse("201501", YearMonth::from)); // works
System.out.println(DateTimeFormatter.ofPattern("yyyyMM'aa'").parse("201501aa", YearMonth::from)); // works
System.out.println(DateTimeFormatter.ofPattern("yyyyMM'00'").parse("20150100", YearMonth::from));
// java.time.format.DateTimeParseException: Text '20150100' could not be parsed at index 0

I debuged the code, it seems the problem is caused by the year field parsing beyond the end of the string (max width for three y's and more is always 19). However, I don't understand how it could work for the pattern without the '00' literal at the end.

Is there any way to fix this withing having to use a formatter builder?

Edit:

Since Jarrod below confirmed it's buggy, I did more googling and finally found the bug reports:

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8031085

http://bugs.java.com/bugdatabase/view_bug.do?bug_id=8032491

Both are only fixed in Java 9 though......

Helmholtz answered 25/8, 2016 at 16:10 Comment(6)
Why don't you want to use a formatter builder?Basidium
Have you tried step debugging through to see what the difference is?Tarshatarshish
@Basidium Because this is a simple case that old tools like SimpleDateFormat supports and the behaviour seems rather like a bug...Helmholtz
SimpleDateFormat fails the same way as well, because they both rely on DateTimeFormatter and it is where the bug is.Tarshatarshish
@Basidium It turns out the case is buggy in the builder as well... See the second bug link.Helmholtz
@JarrodRoberson No, SimpleDateFormat does not fail the same way. It is an independent implementation which does NOT rely on DateTimeFormatter. Just study the old source code of SimpleDateFormat. Both implementations fail for different internal reasons. I would not qualify it as a bug. It is simply a missing feature in both libraries.Kenon
T
8

There is a bug in the DateTimePrinterParser:

I step debugged all the way through it, apparently you can not have digits as literals. Similar test codes proves this if you step debug all the way through to the DateTimeFormatterBuilder.parse() method you can see what it is doing wrong.

Apparently the Value(YearOfEra,4,19,EXCEEDS_PAD) parser consumes the 00 where they stop if those are not digits because it is looking for a number 4 to 19 digits long. The DateTimeFormatter that is embedded in the DateTimeParseContext is wrong.

If you put a non-digit character literal like xx it works, digit literals don't.

Both of these fail:

final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM'00'");
System.out.println(sdf.parse("20150100"));

Exception in thread "main" java.text.ParseException: Unparseable date: "20150100" at java.text.DateFormat.parse(DateFormat.java:366)

final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMM'00'");
System.out.println(dateTimeFormatter.parse("20150100", YearMonth::from));

Exception in thread "main" java.time.format.DateTimeParseException: Text '20150100' could not be parsed at index 0 at java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1949) at java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1851)

Both of these succeed:

final SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM'xx'");
System.out.println(sdf.parse("201501xx"));

Thu Jan 01 00:00:00 EST 2015

final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMM'xx'");
System.out.println(dateTimeFormatter.parse("201501xx", YearMonth::from));

2015-01

Tarshatarshish answered 25/8, 2016 at 16:36 Comment(2)
Thanks for confirming this.Helmholtz
Step debug through the method on the class I mention in the answer, it is too much convoluted logic for me to dig through, but I see what it is doing.Tarshatarshish
K
0

If you don't mind to use a 3rd-party-library then you might try my library Time4J whose newest version v4.18 can do what you wish:

import net.time4j.Month;
import net.time4j.range.CalendarMonth;
import net.time4j.format.expert.ChronoFormatter;
import net.time4j.format.expert.PatternType;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.text.ParseException;
import java.util.Locale;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;

@RunWith(JUnit4.class)
public class CalendarMonthTest {
    @Test
    public void parse2() throws ParseException {
      assertThat(
        ChronoFormatter.ofPattern(
            "yyyyMM'00'",
            PatternType.CLDR,
            Locale.ROOT,
            CalendarMonth.chronology()
        ).parse("20150100"),
        is(CalendarMonth.of(2015, Month.JANUARY)));
    }
}

By the way, the links to the JDK-bug-log are not really related to your problem. Those issues only describe problems when applying adjacent digit parsing in context of fractional seconds. While that problem will be fixed with Java-9, your problem will not. Maybe you wish to open a new issue there? But I doubt that Oracle will treat it as bug. It is rather a new feature not supported until now by any library distributed by Oracle. Literals with (leading) digits are not expected in JSR-310 (aka java.time-package) to take part into adjacent-value-parsing (and in SimpleDateFormat also not).

Side note: Time4J is not just an answer to this detail (digit literals) but generally offers better performance in parsing and can be used in parallel with JSR-310 due to a lot of conversion methods. For example: To achieve an instance of YearMonth, just call calendarMonth.toTemporalAccessor() on the parsed result.

Kenon answered 28/8, 2016 at 5:37 Comment(0)
L
0

As an addendum to user177800's answer, you can use this form instead:

var formatter = new DateTimeFormatterBuilder()
    .appendValue(ChronoField.YEAR, 4)
    .appendValue(ChronoField.MONTH_OF_YEAR, 2)
    .appendLiteral("00")
    .toFormatter();
YearMonth.parse("20220200", formatter);

All part of java.time.

Loftis answered 23/3, 2022 at 18:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.