SimpleDateFormat with German Locale - Java 8 vs Java 10+

I have code and a test-case in a legacy application, which can be summarized as follows:

@Test
public void testParseDate() throws ParseException {
    String toParse = "Mo Aug 18 11:25:26 MESZ +0200 2014";
    String pattern = "EEE MMM dd HH:mm:ss z Z yyyy";

    DateFormat dateFormatter = new SimpleDateFormat(pattern, Locale.GERMANY);
    Date date = dateFormatter.parse(toParse);

    //skipped assumptions
}

This test passes in Java 8 and below. However with Java 10 upwards this leads to a java.text.ParseException: Unparseable date: "Mo Aug 18 11:25:26 MESZ +0200 2014".

For the record: Besides de_DE, the exception is also thrown for the locales de_CH, de_AT, de_LU.

I am aware of the fact, that Date formatting was changed with JDK 9 (JEP 252). However, I consider this to be a disruptive change breaking backwards compatibility. Excerpted:

In JDK 9, the Unicode Consortium's Common Locale Data Repository (CLDR) data is enabled as the default locale data, so that you can use standard locale data without any further action.

In JDK 8, although CLDR locale data is bundled with the JRE, it isn’t enabled by default.

Code that uses locale-sensitive services such as date, time, and number formatting may produce different results with the CLDR locale data.

Adding a . for the day of the week (Mo.) compensates for this and test would pass. However, this is not a real solution for old data (in serialized form such as XML).

Checking this stackoverflow post, it seems that the behaviour is intentional for the German locale and can be mitigated by specifying java.locale.providers with COMPAT mode. However, I do not like the idea to rely on some system property value for two reasons as it might:

  1. change in the next releases of the JDK.
  2. be forgotten in different environments.

My question is:

  • How can I maintain backwards compatibility of legacy code with this particular date pattern, without re-writing / modifying existing serialized data or adding / changing system properties (like java.locale.providers), which may be forgotten in different environments (application servers, standalone jars, ...) ?

Solution 1:

I don’t say it’s a nice solution, but it seems to be a way through.

    Map<Long, String> dayOfWeekTexts = Map.of(1L, "Mo", 2L, "Di", 
            3L, "Mi", 4L, "Do", 5L, "Fr", 6L, "Sa", 7L, "So");
    Map<Long, String> monthTexts = Map.ofEntries(Map.entry(1L, "Jan"), 
            Map.entry(2L, "Feb"), Map.entry(3L, "Mär"), Map.entry(4L, "Apr"),
            Map.entry(5L, "Mai"), Map.entry(6L, "Jun"), Map.entry(7L, "Jul"),
            Map.entry(8L, "Aug"), Map.entry(9L, "Sep"), Map.entry(10L, "Okt"),
            Map.entry(11L, "Nov"), Map.entry(12L, "Dez"));

    DateTimeFormatter formatter = new DateTimeFormatterBuilder()
            .appendText(ChronoField.DAY_OF_WEEK, dayOfWeekTexts)
            .appendLiteral(' ')
            .appendText(ChronoField.MONTH_OF_YEAR, monthTexts)
            .appendPattern(" dd HH:mm:ss z Z yyyy")
            .toFormatter(Locale.GERMANY);

    String toParse = "Mo Aug 18 11:25:26 MESZ +0200 2014";
    OffsetDateTime odt = OffsetDateTime.parse(toParse, formatter);
    System.out.println(odt);
    ZonedDateTime zdt = ZonedDateTime.parse(toParse, formatter);
    System.out.println(zdt);

Output running on my Oracle JDK 10.0.1:

2014-08-18T11:25:26+02:00
2014-08-18T11:25:26+02:00[Europe/Berlin]

Then again, no nice solution may exist.

java.time, the modern Java date and time API, allows us to specify texts to use for fields for both formatting and parsing. So I exploit that for both day of week and for month, specifying the abbreviations without dot that were used with the old COMPAT or JRE locale data. I have used the Java 9 Map.of and Map.ofEntries for building the maps we need. If this is to work in Java 8 too, you must find some other way to populate the two maps, I trust you to do that.

If you do need an old-fashioned java.util.Date (likely in a legacy code base), convert like this:

    Date date = Date.from(odt.toInstant());
    System.out.println("As legacy Date: " + date);

Output in my time zone (Europe/Copenhagen, probably roughly agrees with yours):

As legacy Date: Mon Aug 18 11:25:26 CEST 2014

Suggestion for a strategy

I am thinking that if that were me, I’d consider proceeding this way:

  1. Wait. Set the relevant system property from within Java: System.setProperty("java.locale.providers", "COMPAT,CLDR"); so it won’t be forgot in any environment. The COMPAT locale data have been around since 1.0 (I believe, at least close), so a lot of code out there depends on it (not only yours). The name was changed from JRE to COMPAT in Java 9. To me this may sound like a plan to keep the data around for quite a while still. According to the early access documentation it will still be available in Java 11 (the next “long term support” Java version) and with no deprecation warning or the like. And should it be removed in some future Java version, you will probably be able to find out soon enough that you can deal with the problem before upgrading.
  2. Use my solution above.
  3. Use the locale service provider interface that Basil Bourque linked to. There is no doubt that this is the nice solution in case the COMPAT data should be removed some unknown time in the future. You may even be able to copy the COMPAT locale data into your own files so they can’t take them away from you, only check if there are copyright issues before you do so. The reason why I mention the nice solution last is you said you aren’t happy with having to set a system property in every possible environment where your program may run. As far as I can tell, using your own locale data through the locale service provider interface will still require you to set the same system property (only to a different value).

Solution 2:

Just to mention: SimpleDateFormat is an old way to format dates which BTW is not thread safe. Since Java 8 there are new packages called java.time and java.time.format and you should use those to work with dates. For your purposes you should use class DateTimeFormatter.