DateTimeFormatter.parse(s) throws when it reaches the end of s: why?

Viewed 284

I've been trying to use DateTimeFormatter to parse date/time strings of the form "20141112152340". When I call formatter.parse(input); it raises an exception:

java.time.format.DateTimeParseException: Text '20141112152340' could not be parsed at index 14

Here is a minimal complete reproducer:

package com.example.minimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.ResolverStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;

public class Reproducer {
    public static void main(String[] args) {
        DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendValue(ChronoField.YEAR) // 4
            .appendValue(ChronoField.MONTH_OF_YEAR) // 2
            .appendValue(ChronoField.DAY_OF_MONTH) // 2
            .appendValue(ChronoField.HOUR_OF_DAY) // 2
            .appendValue(ChronoField.MINUTE_OF_HOUR) // 2
            .appendValue(ChronoField.SECOND_OF_MINUTE) // 2
            .toFormatter()
            // Using the default ResolverStyle appears to make no difference.
            .withResolverStyle(ResolverStyle.STRICT); // expect 14 characters
        String input = "20141112152340";
        System.out.printf("length of input is %02d:               [%s]; format is %s\n", input.length(), input, formatter.toString());
        // When the minute of hour is < 10, it is rendered without zero-padding.   Perhaps this
        // is related.  But, the same problem is reproducible on input strings that do not contain zeroes.
        System.out.printf("Current local time in that format is [%s]\n", formatter.format(LocalDateTime.now()));
        // The following line throws java.time.format.DateTimeParseException: Text '20140816152340' could not be parsed at index 14
        TemporalAccessor parsed = formatter.parse(input);
        // NOTREACHED
        LocalDateTime stamp = parsed.query(LocalDateTime::from);
        System.out.println("Time stamp is " + stamp.toString());
    }
}

Here is the output of this program:

length of input is 14:               [20141112152340]; format is Value(Year)Value(MonthOfYear)Value(DayOfMonth)Value(HourOfDay)Value(MinuteOfHour)Value(SecondOfMinute)
Current local time in that format is [20191010183335]
Exception in thread "main" java.time.format.DateTimeParseException: Text '20141112152340' could not be parsed at index 14
    at java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:2046)
    at java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1874)
    at com.example.minimal.Reproducer.main(Reproducer.java:28)

As far as I can tell, the parser is bailing at the point it reaches the end of the string. On the offchance that this happens because it's looking for more data I tried switching the resolver style from the default to strict, but this seems to make no difference.

My question is, why is this happening, and how do I correct my code so that it can parse this date/time representation?

4 Answers

The following formatter does work for me:

DateTimeFormatter.ofPattern("yyyyMMddHHmmss");

The toString() of both formatters reveals the difference:

Value(Year)Value(MonthOfYear)Value(DayOfMonth)Value(HourOfDay)Value(MinuteOfHour)Value(SecondOfMinute)

vs

Value(YearOfEra,4,19,EXCEEDS_PAD)Value(MonthOfYear,2)Value(DayOfMonth,2)Value(HourOfDay,2)Value(MinuteOfHour,2)Value(SecondOfMinute,2)

As you can see, the second formatter, which is the working version, seems to have explicit field widths as well as explicit SignStyle

We can create the same formatter using the builder:

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
        .appendValue(ChronoField.YEAR_OF_ERA, 4, 19, SignStyle.EXCEEDS_PAD)
        .appendValue(ChronoField.MONTH_OF_YEAR, 2)
        .appendValue(ChronoField.DAY_OF_MONTH, 2)
        .appendValue(ChronoField.HOUR_OF_DAY, 2)
        .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
        .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
        .toFormatter()
        .withResolverStyle(ResolverStyle.STRICT);

However it looks like you can replace YEAR_OF_ERA with YEAR with a similar result.

To get this to work unambiguously you need to explicitly specify the width of each part:

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
        .appendValue(ChronoField.YEAR, 4)
        .appendValue(ChronoField.MONTH_OF_YEAR, 2)
        .appendValue(ChronoField.DAY_OF_MONTH, 2)
        .appendValue(ChronoField.HOUR_OF_DAY, 2)
        .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
        .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
        .toFormatter()

appendValue method accepts a second argument that is the size of the field. Like:

.appendValue(ChronoField.YEAR, 4) // 4

I put the sizes in all values and it worded fine.

The other answers have already suggested working corrections.

My question is, why is this happening, …

This is because the parser grabs all 14 digits for year, and then there are no digits for the rest. So when it tries to parse month, it objects and says that it cannot parse the month at index 14, the end of the string.

From the documentation of the appendValue method that you are using:

The parser for a variable width value such as this normally behaves greedily, requiring one digit, but accepting as many digits as possible. This behavior can be affected by 'adjacent value parsing'. See appendValue(java.time.temporal.TemporalField, int) for full details.

The documentation of the two-argument appendValue method adds:

No action is required to initiate 'adjacent value parsing'. When a call to appendValue is made, the builder enters adjacent value parsing setup mode. If the immediately subsequent method call or calls on the same builder are for a fixed width value, then the parser will reserve space so that the fixed width values can be parsed.

So this means that you need to specify fields widths for all fields except the first. I tried this formatter with your code, and it works:

    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendValue(ChronoField.YEAR) // 4
            .appendValue(ChronoField.MONTH_OF_YEAR, 2)
            .appendValue(ChronoField.DAY_OF_MONTH, 2)
            .appendValue(ChronoField.HOUR_OF_DAY, 2)
            .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
            .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
            .toFormatter();

I have not specified the width of the year, but for all other fields. This is enough that when there are 14 digits, the formatter figures out that it is allowed to take 4 of them for year. I also tried it with strings of length 12 and 15, and it takes 2 and 5 digits for year in those respective cases.

You are right that the resolver style doesn’t make any difference. Resolving is the process of interpreting the parsed numbers into date and time. So strict resolver style will cause February 29 to be rejected if the year is not a leap year. But in your case parsing goes wrong already before any resolution is tried.

Related