2

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.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
Stefan S.
  • 3,950
  • 5
  • 25
  • 77
  • Please take a look here - https://stackoverflow.com/questions/30816016/how-to-parse-non-standard-month-names-with-datetimeformatter – vinS Dec 12 '17 at 12:53
  • @vinS That is exactly the opposite of what I want. I want to parse the standard German date. – Stefan S. Dec 12 '17 at 12:55
  • 1
    I tried your code with JDK-9. It returns me similar exception with three minor differences: java.base/java.time.format.DateTimeFormatter.parseResolved0(DateTimeFormatter.java:1988) java.base/java.time.format.DateTimeFormatter.parse(DateTimeFormatter.java:1890) java.base/java.time.LocalDate.parse(LocalDate.java:428) I think you can't resolve this without checking your string. – zlakad Dec 12 '17 at 13:31
  • 1
    There is not only one single standard German date. In reality, Germans use many forms so you cannot totally rely on single formats. – Meno Hochschild Dec 12 '17 at 20:56

2 Answers2

2

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
Ole V.V.
  • 81,772
  • 15
  • 137
  • 161
  • 1
    Indeed, this seems to be an assumption about German cultural norms including a padding zero. Some internet searching shows that the modern approach in Germany proper is generally to include the padding zero when representing in all digits (no month name). Past traditional habits may have varied, as do other German-related places. So this behavior in Java appears to be a feature, not a bug — but some folks may expect other behavior. Wikipedia: [here](https://en.m.wikipedia.org/wiki/Date_format_by_country) and [here](https://en.m.wikipedia.org/wiki/Date_and_time_notation_in_Europe). – Basil Bourque Dec 12 '17 at 22:54
  • 2
    @BasilBourque Indeed, just a feature and not a bug. Java relies here on CLDR-data, and they had to choose one wide-spread variation with preference to [German DIN-norm 5008](https://de.wikipedia.org/wiki/Datumsformat). This is also what I feel most common (as my mother language is German). – Meno Hochschild Dec 13 '17 at 03:05
  • @BasilBourque I don't know about the feature thing - the old API supported both date formats: `DateFormat.getDateInstance(DateFormat.SHORT, Locale.GERMAN).parse("1.4.17")` And the Wiki articles both state that the zero was only used as padding and both are possible. – Stefan S. Dec 13 '17 at 06:47
  • 1
    @SteffiS. The old `DateFormat` accepted a wealth of incorrect input, this was most often a nuissance because you didn’t catch bugs. You’ve found a way to exploit it as a feature. – Ole V.V. Dec 13 '17 at 06:49
  • 1
    @OleV.V. "Exploit" is a pretty harsh word for "allowing the date format that comes naturally to most people". – Stefan S. Dec 13 '17 at 06:53
  • Sorry about my clumsy English, @SteffiS. – Ole V.V. Dec 13 '17 at 07:27
  • You can find the "default" date formats for various locales in the `sun.text.resources` resource bundles. For Germany, see http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8-b132/sun/text/resources/de/FormatData_de.java?av=f. I haven't looked at java 9 or other java implementation but suspect it would be similar. – Michael McKay Dec 13 '17 at 14:13
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
Michael McKay
  • 650
  • 4
  • 11
  • I even suspect that using the reduced pattern from the outset would work. – Ole V.V. Dec 13 '17 at 04:06
  • This will work for all locales using two-digit day-og-month and/or month (not only German). The OP will have to decide whether this is desired; it could be an advantage. If it is indeed desired, your solution seems less of a hack than mine. – Ole V.V. Dec 13 '17 at 06:17