DateTimeFormatter Cannot Parse Dates Without Leadi

2019-05-25 04:05发布

问题:

Our client found an interesting bug today. Consider the following method:

final DateTimeFormatter englishFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(Locale.ENGLISH);
System.out.println(LocalDate.parse("04/01/17", englishFormatter));
System.out.println(LocalDate.parse("4/1/17", englishFormatter));

final DateTimeFormatter germanFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(Locale.GERMAN);
System.out.println(LocalDate.parse("01.04.17", germanFormatter));
System.out.println(LocalDate.parse("1.4.17", germanFormatter));

(If you don't know, these are indeed correct dates in English and German. I'd even say they're the same date [April 1 2017]. Take a moment to consider what you'd think this application should return.)

What it does return is the following:

Exception in thread "main" java.time.format.DateTimeParseException: Text '1.4.17' 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)
    at java.time.LocalDate.parse(LocalDate.java:400)
    at Main.main(Main.java:20)

The English date format works with and without leading zeroes. The German format works only with leading zeroes.

I can't seem to find a property to change this behavior to work correctly.

How can I make the DateTimeFormatter understand German dates without leading zeroes?

Note: Our application supports multiple locales, so using a specific DateTimeFormatter (like DateTimeFormatter.ofPattern("d.M.yy")) is completely out of the question, especially since we want to parse the default date format.

回答1:

I tried the opposite: formatting the date with your localized formatters.

    LocalDate testDate = LocalDate.of(2017, Month.APRIL, 1);

    final DateTimeFormatter englishFormatter 
            = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
                .withLocale(Locale.ENGLISH);
    System.out.println("English: " + testDate.format(englishFormatter));

    final DateTimeFormatter germanFormatter 
            = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
                .withLocale(Locale.GERMAN);
    System.out.println("German: " + testDate.format(germanFormatter));

I got

English: 4/1/17
German:  01.04.17

So it’s quite clear that Java thinks that standard German date formatting uses leading zeroes. If you are certain that this is wrong, you may consider filing a bug with Oracle.

To circumvent the behaviour you don’t like, with multiple locales I am afraid you will need some sort of hack. The best hack I could think of was the following. It’s not beautiful. It works.

private static final Map<Locale, DateTimeFormatter> STEFFI_S_LOCALIZED_FORMATTERS
        = createSteffiSFormatters();

private static Map<Locale, DateTimeFormatter> createSteffiSFormatters() {
    Map<Locale, DateTimeFormatter> formatters = new HashMap<>(2);
    formatters.put(Locale.GERMAN, DateTimeFormatter.ofPattern("d.M.uu"));
    return formatters;
}

public static DateTimeFormatter getLocalizedFormatter(Locale formattingLocale) {
    DateTimeFormatter localizedFormatter
            = STEFFI_S_LOCALIZED_FORMATTERS.get(formattingLocale);
    if (localizedFormatter == null) {
        localizedFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)
                                              .withLocale(formattingLocale);
    }
    return localizedFormatter;
}

Now you can do:

    final DateTimeFormatter englishFormatter = getLocalizedFormatter(Locale.ENGLISH);
    System.out.println(LocalDate.parse("04/01/17", englishFormatter));
    System.out.println(LocalDate.parse("4/1/17", englishFormatter));

    final DateTimeFormatter germanFormatter = getLocalizedFormatter(Locale.GERMAN);
    System.out.println(LocalDate.parse("01.04.17", germanFormatter));
    System.out.println(LocalDate.parse("1.4.17", germanFormatter));

This prints:

2017-04-01
2017-04-01
2017-04-01
2017-04-01


回答2:

One solution is to trap the DateTimeParseException and then try again with a modified/reduced date pattern.

import java.text.Format;
import java.time.LocalDate;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.FormatStyle;
import java.util.ListResourceBundle;
import java.util.Locale;

public class Demo {
    public static void main(String[] args) {

        Locale[] locales = {Locale.ENGLISH, Locale.GERMAN};
        for (Locale locale : locales)
        {
            DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT).withLocale(locale);
            String [] englishDates = {"01/04/17","1/4/17"};
            String [] germanDates = {"01.04.17","1.4.17"};

            String datePattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.SHORT,null, IsoChronology.INSTANCE, locale);
            System.out.println("Locale " + locale.getDisplayName() + ": Default Date Pattern (short): " + datePattern);

            String[] dates = null;

            if (locale == Locale.ENGLISH)
                dates = englishDates;
            else
                dates = germanDates;

            for (String date : dates) {
                try {
                    System.out.printf("  " + date + " -> ");
                    System.out.println(LocalDate.parse(date, formatter));
                }
                catch (DateTimeParseException e)
                {
                    System.out.println("Error!");
                    // Try alternate pattern
                    datePattern = datePattern.replace("dd", "d").replace("MM", "M");
                    System.out.println("  Modified Date Pattern (short): " + datePattern);
                    // Allow single digits in day and month
                    DateTimeFormatter modifiedFormatter = DateTimeFormatter.ofPattern(datePattern);
                    System.out.println("  " + date + " -> " + LocalDate.parse(date, modifiedFormatter));  // OK
                }
            }
        }
    }
}

This gives:

Locale English: Default Date Pattern (short): M/d/yy
  01/04/17 -> 2017-01-04
  1/4/17 -> 2017-01-04
Locale German: Default Date Pattern (short): dd.MM.yy
  01.04.17 -> 2017-04-01
  1.4.17 -> Error!
  Modified Date Pattern (short): d.M.yy
  1.4.17 -> 2017-04-01