Home > Back-end >  Is it possible write a custom date format in a way that works for all locales in Java?
Is it possible write a custom date format in a way that works for all locales in Java?

Time:01-04

My goal is to display a date in a format like:

Country example pattern
US Tue, Jan 3 EEE, LLL d
France mar. 3 janv. EEE d LLL

I'm using DateTimeFormatter.ofPattern to format my dates.

Is there a way to implement this without writing the pattern for every locale?

Edit: I'll be very specific with my use case: I'm trying to format a LocalDate to implement this date picker: m3.material.io/components/date-pickers/…

CodePudding user response:

Not as a pattern, no. A pattern encodes order, for example - it is not possible to have one pattern such that 'in french' it'll stick the month after the date-of-month, and 'in english' it'll be reversed. At best, the localization of a single pattern translates names; a single pattern can produce both Tue, Jan 3 as well as Mar., janv. 3. Which isn't what you want.

The method you're looking for is ofLocalizedDate(dateStyle). This produces a DTF instance that is 'lazy' - it doesn't have a pattern, it simply knows 'I need whatever passes for 'medium standard format' in locale X' where X is the locale of the DTF.

There is no pattern that encodes this behaviour.

for (var locale : List.of(Locale.ENGLISH, Locale.FRENCH)) {
  for (var style : FormatStyle.values()) {
    var dtf = DateTimeFormatter.ofLocalizedDate(style).withLocale(locale);
    System.out.printf("%s %s: %s\n", locale, style, dtf.format(LocalDate.of(2023, 1, 3)));
  }
}

ends up printing:

en FULL: Tuesday, January 3, 2023
en LONG: January 3, 2023
en MEDIUM: Jan 3, 2023
en SHORT: 1/3/23
fr FULL: mardi 3 janvier 2023
fr LONG: 3 janvier 2023
fr MEDIUM: 3 janv. 2023
fr SHORT: 03/01/2023

Which shows the dynamic nature of this ofLocalizedDate variant of DTF.

A thing to keep in mind that DateTimeFormatter is not necessarily a pattern-based concept. Patterns are merely one way to make them. That way does not allow you to change what they look like by calling .withLocale. Other ways of making DTFs, such as .ofLocalizedDate, do allow it.

If you're storing a thing in a database or asking the user for something, then you'd have to make a divergent option: The user first picks if they want 'pattern' or 'style', so you store this (an enum col in your DB, or a radiobutton set in your UI), and then depending on what they picked, either store a style value (FULL, LONG, MEDIUM, or SHORT) - again a radio buttonset in a UI or again an enum in a db col, or, you store the pattern (string/VARCHAR/TEXT/JTextField, something along those lines). Depending on the 'style' you construct a DTF instance from such stored preferences by checking for the 'variant' option (style or pattern) and invoking either .ofPattern or .ofLocalizedDate. While you're at it, you might want that original radiobutton set ('pattern' or 'style') to also include ISO8601 style.

NB: There is a reason the DTF class does not have a .getPattern() method; that's because not all DTF instances have one.

CodePudding user response:

I think it's very difficult without manually enumerating them. The JDK uses Locale data from Unicode, so it's limited by which formats are in its source data, for example these: https://github.com/openjdk/jdk/blob/master/make/data/cldr/common/main/en_GB.xml#L144

<dateFormats>
    <dateFormatLength type="full">
        <dateFormat>
            <pattern>EEEE, MMMM d, y G</pattern>
            <datetimeSkeleton>GyMMMMEEEEd</datetimeSkeleton>
        </dateFormat>
    </dateFormatLength>
    <dateFormatLength type="long">
        <dateFormat>
            <pattern>MMMM d, y G</pattern>
            <datetimeSkeleton>GyMMMMd</datetimeSkeleton>
        </dateFormat>
    </dateFormatLength>
    <dateFormatLength type="medium">
        <dateFormat>
            <pattern>MMM d, y G</pattern>
            <datetimeSkeleton>GyMMMd</datetimeSkeleton>
        </dateFormat>
    </dateFormatLength>
    <dateFormatLength type="short">
        <dateFormat>
            <pattern>M/d/y GGGGG</pattern>
            <datetimeSkeleton>GGGGGyMd</datetimeSkeleton>
        </dateFormat>
    </dateFormatLength>
</dateFormats>

The only date format which includes day-of-week at all is the so-called "full" format, and that uses the entire word, not an abbreviation for the word.

With that in mind, here's a horrendously hacky solution that works for your 2 examples, and probably fails for lots of others. If you want to use it, I'd suggest unit testing it with a bunch of common Locales.

It works by getting the "full" pattern and manipulating it a bit.

private static DateTimeFormatter getLocalDayMonthFormatter(Locale locale) {
    String localizedDateTimePattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern(
        FormatStyle.FULL, null, Chronology.ofLocale(locale), locale
    );
    String pattern = localizedDateTimePattern
        .replaceAll("[^MdE\\s,.]", " ")    // Remove anything except month, days, spaces, commas and periods
        .replaceAll("(.)\\1{3}", "$1$1$1") // Replace any char that occurs 4 times with 3 of them
        .replaceAll("\\s ", " ")           // Conflate spaces
        .trim()
        .replaceAll(",$", "");             // If there's a trailing comma, e.g. because of removal of the year, delete it
    return DateTimeFormatter.ofPattern(pattern, locale);
}

Sample usage:

DateTimeFormatter formatter = getLocalDayMonthFormatter(Locale.US);
System.out.println(formatter.format(LocalDate.now()));

CodePudding user response:

If you have an internationalized application, just have an internationalized version of your date pattern. Depending on the configured locale the right formatting would be pulled and used to format your date.

In case of an internationalized application you are doing this for all the strings the UI would display anyway, so it is a piece of cake.

  • Related