4

I have been struggling with how java 8 handles the parsing of a String into an instance of LocalDateTime. I have noticed it when I was writing an unit test for that conversion from String to LocalDateTime.

Here below is a method example (not actual) which I have been used for a while since java 8 got released.

private void convertToLocalDateTime(String s) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-M-d HH:mm")
        .withResolverStyle(ResolverStyle.STRICT);
    try {
        LocalDateTime ldt = LocalDateTime.parse(s, formatter);
        System.out.println("s: " + s + " -> ldt: " + ldt.toString());
    } catch(DateTimeParseException e) {
        System.out.println("s: " + s + " -> The field is not a valid date.");
    }
}

Here, you can see that I have added two s.o.p's to show you the display. Here is an display example;

s: 2016-06-31 11:00 -> ldt: 2016-06-30T11:00
s: 2016-07-31 11:00 -> ldt: 2016-07-31T11:00
s: 2016-08-31 11:00 -> ldt: 2016-08-31T11:00
s: 2016-09-31 11:00 -> ldt: 2016-09-30T11:00
s: 2016-10-31 11:00 -> ldt: 2016-10-31T11:00
s: 2016-11-31 11:00 -> ldt: 2016-11-30T11:00
s: 2016-12-31 11:00 -> ldt: 2016-12-31T11:00
s: 2016-11-0 11:00 -> The field is not a valid date.
s: 2016-11-1 11:00 -> ldt: 2016-11-01T11:00
s: 2016-11-15 11:00 -> ldt: 2016-11-15T11:00
s: 2016-11-30 11:00 -> ldt: 2016-11-30T11:00
s: 2016-11-31 11:00 -> ldt: 2016-11-30T11:00
s: 2016-11-32 11:00 -> The field is not a valid date.

As you can see everything seems correct, however the dates with days that is 1 day incorrect (eg 31 November) gets converted to 30 November. I was quite baffled because I've been using this approach the whole time and I just noticed that today.

After doing some research, I found out that there is a "resolver style". Here is a text from the java doc

public static DateTimeFormatter ofPattern(String pattern) Creates a formatter using the specified pattern. This method will create a formatter based on a simple pattern of letters and symbols as described in the class documentation. For example, d MMM uuuu will format 2011-12-03 as '3 Dec 2011'.

The formatter will use the default FORMAT locale. This can be changed using withLocale(Locale) on the returned formatter Alternatively use the ofPattern(String, Locale) variant of this method.

The returned formatter has no override chronology or zone. It uses SMART resolver style.

I was like "wut". Apparently, there are three resolver styles: STRICT, SMART and LENIENT.

So, I went with the STRICT. The first line in the body of the convertToLocalDateTime(String) method is modified with

DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-M-d HH:mm")
    .withResolverStyle(ResolverStyle.STRICT);

Yet this gives the following

s: 2016-01-31 11:00 -> The field is not a valid date.
s: 2016-02-31 11:00 -> The field is not a valid date.
s: 2016-03-31 11:00 -> The field is not a valid date.
s: 2016-04-31 11:00 -> The field is not a valid date.
s: 2016-05-31 11:00 -> The field is not a valid date.
s: 2016-06-31 11:00 -> The field is not a valid date.
s: 2016-07-31 11:00 -> The field is not a valid date.
s: 2016-08-31 11:00 -> The field is not a valid date.
s: 2016-09-31 11:00 -> The field is not a valid date.
s: 2016-10-31 11:00 -> The field is not a valid date.
s: 2016-11-31 11:00 -> The field is not a valid date.
s: 2016-12-31 11:00 -> The field is not a valid date.
s: 2016-11-0 11:00 -> The field is not a valid date.
s: 2016-11-1 11:00 -> The field is not a valid date.
s: 2016-11-15 11:00 -> The field is not a valid date.
s: 2016-11-30 11:00 -> The field is not a valid date.
s: 2016-11-31 11:00 -> The field is not a valid date.
s: 2016-11-32 11:00 -> The field is not a valid date.

These all suddenly doesn't work. Even using yyyy-MM-dd HH:mm doesn't help. I suspect the STRICT doesn't work with custom format strings very well...

So, my question is: Is there a way to overcome the "31 november" issue, that it does not get resolved to 30 november? (and ofc similar for other possible dates) Or a way to return false if the resolver has adjusted the values?

Or do I have to evaluate the resulting LocalDateTime instance myself?

Nicolas Filotto
  • 43,537
  • 11
  • 94
  • 122
KarelG
  • 5,176
  • 4
  • 33
  • 49

1 Answers1

4

Your confusion is justified. I find that in fact a DateTimeFormatter for pattern "yyyy-MM-dd HH:mm", using the STRICT resolver style, rejects even date strings that it formatted itself.

Upon further investigation, this seems to be related to DateTimeFormatter's new (relative to the well-established java.text.SimpleDateFormat) distinction between "year" and "year of era". With DateTimeFormatter, the 'y' format symbol represents the latter; the former corresponds to format letter 'u'. It turns out that if I use the format string "uuuu-MM-dd HH:mm" that your program produces the output you seem to be looking for:

private void convertToLocalDateTime(String s) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-M-d HH:mm")
        .withResolverStyle(ResolverStyle.STRICT);
    try {
        LocalDateTime ldt = LocalDateTime.parse(s, formatter);
        System.out.println("s: " + s + " -> ldt: " + ldt.toString());
    } catch(DateTimeParseException e) {
        System.out.println("s: " + s + " -> The field is not a valid date.");
    }
}

Single-digit vs. double-digit fields does not make a difference for parsing.

This question addresses the difference between "year" and "year of era". Whereas I understand the distinction, I think it was a poor choice to implement DateTimeFormatter with such unnecessary incompatibility with SimpleDateFormat for some of their most common uses. I think I would have swapped the meaning of 'u' and 'y', so as to avoid exactly the kind of problem you've run into. Evidently, however, the designers of this API disagreed, and that's all water under the bridge at this point.

Community
  • 1
  • 1
John Bollinger
  • 160,171
  • 8
  • 81
  • 157