DateTimeFormatter month pattern letter "L" fails

I noticed that java.time.format.DateTimeFormatter is not able to parse out as expected. See below:

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class Play {
  public static void tryParse(String d,String f) {
    try { 
      LocalDate.parse(d, DateTimeFormatter.ofPattern(f)); 
      System.out.println("Pass");
    } catch (Exception x) {System.out.println("Fail");}
  }
  public static void main(String[] args) {
    tryParse("26-may-2015","dd-L-yyyy");
    tryParse("26-May-2015","dd-L-yyyy");
    tryParse("26-may-2015","dd-LLL-yyyy");
    tryParse("26-May-2015","dd-LLL-yyyy");
    tryParse("26-may-2015","dd-M-yyyy");
    tryParse("26-May-2015","dd-M-yyyy");
    tryParse("26-may-2015","dd-MMM-yyyy");
    tryParse("26-May-2015","dd-MMM-yyyy");
  }
}

Only the last attempt with tryParse("26-May-2015","dd-MMM-yyyy"); will "Pass". As per the documentation LLL should be able to parse out textual format. Also note the subtle difference of the uppercase 'M' vs lowercase 'm'.

This is really annoying, as I cannot by default parse out strings formatted by default by Oracle DB

SELECT TO_DATE(SYSDATE,'DD-MON-YYYY') AS dt FROM DUAL;

Similarly, for following program:

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

public class Play {
  public static void output(String f) {
    LocalDate d = LocalDate.now();
    Locale l = Locale.US;
    // Locale l = Locale.forLanguageTag("ru");
    System.out.println(d.format(DateTimeFormatter.ofPattern(f,l)));
  }
  public static void main(String[] args) {
    output("dd-L-yyyy");
    output("dd-LLL-yyyy");
    output("dd-M-yyyy");
    output("dd-MMM-yyyy");
  }
}

I get below output:

28-5-2015
28-5-2015
28-5-2015
28-May-2015

Clearly the L Format specifier doesn't treat anything textual, seems numeric to me ...

However, if I change the Locale to Locale.forLanguageTag("ru"), I get the following output:

28-5-2015
28-Май-2015
28-5-2015
28-мая-2015

All really interesting, wouldn't you agree?

The questions I have are:

  • Is it reasonable for me to expect that each of the should work?
  • Should we at least submit some of these as a bug?
  • Do I misunderstand the usage of the L pattern specifier.

Quoting a part from the documentation that I percieved as 'it matters':

Text: The text style is determined based on the number of pattern letters used. Less than 4 pattern letters will use the short form. Exactly 4 pattern letters will use the full form. Exactly 5 pattern letters will use the narrow form. Pattern letters 'L', 'c', and 'q' specify the stand-alone form of the text styles.

Number: If the count of letters is one, then the value is output using the minimum number of digits and without padding. Otherwise, the count of digits is used as the width of the output field, with the value zero-padded as necessary. The following pattern letters have constraints on the count of letters. Only one letter of 'c' and 'F' can be specified. Up to two letters of 'd', 'H', 'h', 'K', 'k', 'm', and 's' can be specified. Up to three letters of 'D' can be specified.

Number/Text: If the count of pattern letters is 3 or greater, use the Text rules above. Otherwise use the Number rules above.

UPDATE

I have made two submissions to Oracle:

  • Request for Bugfix for the LLL (Long Form Text) issue: JDK-8114833 (original oracle Review ID: JI-9021661)
  • Request for enhancement for the lowercase month parsing issue: Review ID: 0 (is that also a bug??)

Solution 1:

“stand-alone” month name

I believe 'L' is meant for languages that use a different word for the month itself versus the way it is used in a date. For example:

Locale russian = Locale.forLanguageTag("ru");

asList("MMMM", "LLLL").forEach(ptrn -> 
    System.out.println(ptrn + ": " + ofPattern(ptrn, russian).format(Month.MARCH))
);

Output:

MMMM: марта
LLLL: Март

There shouldn't be any reason to use 'L' instead of 'M' when parsing a date.

I tried the following to see which locales support stand-alone month name formatting:

Arrays.stream(Locale.getAvailableLocales())
    .collect(partitioningBy(
                loc -> "3".equals(Month.MARCH.getDisplayName(FULL_STANDALONE, loc)),
                mapping(Locale::getDisplayLanguage, toCollection(TreeSet::new))
    )).entrySet().forEach(System.out::println);

The following languages get a locale-specific stand-alone month name from 'LLLL':

Catalan, Chinese, Croatian, Czech, Finnish, Greek, Hungarian, Italian, Lithuanian, Norwegian, Polish, Romanian, Russian, Slovak, Turkish, Ukrainian

All other languages get "3" as a stand-alone name for March.

Solution 2:

According to the javadocs:

Pattern letters 'L', 'c', and 'q' specify the stand-alone form of the text styles.

However, I couldn't find much about what the "stand-alone" form is supposed to be. In looking at the code I see that using 'L' selects TextStyle.SHORT_STANDALONE and according to that javadoc:

Short text for stand-alone use, typically an abbreviation. For example, day-of-week Monday might output "Mon".

However, that isn't how it seems to work. Even with three letters I get numerical output from this code:

DateTimeFormatter pattern = DateTimeFormatter.ofPattern ("dd-LLL-yyyy");
System.out.println (pattern.format (LocalDate.now ()));

Edit

After further investigation it seems (as near as I can tell) that the "stand-alone" versions of these codes are for when you want to load your own locale-independent data, presumably using DateTimeFormatterBuilder. As such, by default DateTimeFormatter has no entries loaded for TextStyle.SHORT_STANDALONE.

Solution 3:

While the other answers give excellent information on pattern letter L and date parsing, I should like to add that you should really avoid the problem altogether. Don’t get date (and time) as string from your database. Instead use an appropriate datetime object.

    String sql = "select sysdate as dt from dual;"
    PreparedStatement stmt = yourDatabaseConnection.prepareStatement(sql);
    ResultSet rs = stmt.executeQuery();
    if (rs.next()) {
        LocalDateTime dateTime = rs.getObject("dt", LocalDateTime.class);
        // do something with dateTime
    }

(Not tested since I haven’t got an Oracle database at hand. Please forgive any typo.)