Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When I format a LocalDate to dd.MMM.YYYY I get 01.Jan..2000 with two dots

I am trying to format LocalDate variables to dd.MMM.YYYY with:

DateTimeFormatter.ofPattern("dd.MMM.yyyy")

The problem is that more half the time I get two dots. For example 01-01-2000 goes to 01.Jan..2000.

I know why I have this problem, because of the three Ms. When I use dd.MM.yyyy I get to 01.01.2000 without issue. The third M is the problem.

How can I fix this?

like image 476
powqmnbvc21 Avatar asked Dec 30 '22 12:12

powqmnbvc21


2 Answers

The cause of your problem is that the abbreviations for months are locale specific:

  • In some locales there is a dot (period) to indicate abbreviations1; Locale.CANADA for example. In others there isn't; Locale.ENGLISH for example.
  • In the locales where a dot indicates abbreviation, you may or may not find that there is dot when the name of months doesn't need abbreviating. For example the name of the month May is only three letters, so May. indicating that this is an abbreviation would be illogical.

There are various ways to deal with this, including:

  1. Don't fix it. The output with doubled dots in some cases and not others is logically correct (by a certain logic)2, even though it looks odd.

  2. My preferred way would be to use a different output format. Don't use dot as a separator.

    Using dot characters as separators is ... unconventional ... and when you combine this with abbreviated month names, you get this awkward edge-case.

    Sure there are ways to deal with this, but consider that other people might then run into an equivalent problem if they need to parse your dates in their code-base.

  3. Hard wire your DateTimeFormatter to an existing Locale where there are no dots in the abbreviated names.

    There is a theoretical risk that they may decide to change the abbreviations in a standard Locale. But doubt that they would, because such a change is liable to break customer code which is implicitly locale dependent ... like yours would be.

  4. Create a custom Locale and use that when creating the DateTimeFormatter.

  5. Use DateTimeFormatterBuilder for create the formatter. To deal with the month, use appendText(TemporalField field, Map<Long,String> textLookup) with a lookup table that contains exactly the abbreviations that you want to use.

    Depending on how you "append" the other fields, your formatter can be totally or partially locale independent.


Of these, 2. and 5. are the most "correct", in my opinion. Ole's answer illustrates these options with code.


1 - See this article on American English grammar - When you need periods after abbreviations.
2 - The problem would be convincing people that "looks odd but is logical" is better than "looks nice but is illogical". I don't think you would win this argument ...

like image 126
Stephen C Avatar answered Jan 14 '23 15:01

Stephen C


Stephen C. has written an answer that covers your options really well. As a supplement, since I agree that options 2 and 5 are the most correct, I would like to spell those two out.

Option 2: Use a different format

Localized date formats for most available locales are built into Java. These are generally under-used. We can save ourselves a lot if trouble by relying on Java to know how to format dates for our audience and their locale. I am using German as an example because it’s one of those locales that consistently includes dots both between the parts of the date and for abbreviation. The following should work for your locale too even if it’s not German (if you substitute Locale.getDefault(Locale.Category.FORMAT) or your users’ locale).

private static final Locale LOCALE = Locale.GERMAN;

private static final DateTimeFormatter DATE_FORMATTER
        = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
                .withLocale(LOCALE);

For demonstration I am formatting a day of each month of the current year:

    LocalDate date = LocalDate.of(2021, Month.JANUARY, 16);
    for (int i = 0; i < 12; i++) {
        System.out.println(date.format(DATE_FORMATTER));
        date = date.plusMonths(1).minusDays(1);
    }

Output is:

16.01.2021
15.02.2021
14.03.2021
13.04.2021
12.05.2021
11.06.2021
10.07.2021
09.08.2021
08.09.2021
07.10.2021
06.11.2021
05.12.2021

For German locale we got numeric months here. Other locales may give other results, for example month abbreviations.

If you want a longer format that doesn’t use numeric months, specify for example FormatStyle.LONG instead of FormatStyle.MEDIUM:

private static final DateTimeFormatter DATE_FORMATTER
        = DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)
                .withLocale(LOCALE);

16. Januar 2021
15. Februar 2021
14. März 2021
13. April 2021
12. Mai 2021
11. Juni 2021
10. Juli 2021
9. August 2021
8. September 2021
7. Oktober 2021
6. November 2021
5. Dezember 2021

I suggest that your users would be happy with one of the above.

Option 5: DateTimeFormatterBuilder.appendText(TemporalField, Map<Long, String>)

If your users tell you that they don’t want the localized formats above and they do want your format with month abbreviations and single dots — it’s getting longer, but the result is beautiful and everyone will be happy.

private static final DateTimeFormatter DATE_FORMATTER = new DateTimeFormatterBuilder()
        .appendPattern("dd.")
        .appendText(ChronoField.MONTH_OF_YEAR, getMonthAbbreviations())
        .appendPattern(".uuuu")
        .toFormatter(LOCALE);

private static Map<Long, String> getMonthAbbreviations() {
    return Arrays.stream(Month.values())
            .collect(Collectors.toMap(m -> Long.valueOf(m.getValue()),
                    MyClass::getDisplayNameWithoutDot));
}

private static String getDisplayNameWithoutDot(Month m) {
    return m.getDisplayName(TextStyle.SHORT, LOCALE)
            .replaceFirst("\\.$", "");
}

Output from the same loop as above:

16.Jan.2021
15.Feb.2021
14.März.2021
13.Apr.2021
12.Mai.2021
11.Juni.2021
10.Juli.2021
09.Aug.2021
08.Sep.2021
07.Okt.2021
06.Nov.2021
05.Dez.2021

One dot each time. The central trick is to use Java’s month abbreviation and remove the dot from it if there is one (Jan. becomes Jan) and use it as-is if there is no dot (Mai stays Mai). My getDisplayNameWithoutDot method does this. I am in turn using this method to build the map that the two-arg appendText(TemporalField, Map<Long, String>) method requires and uses for formatting.

like image 34
Ole V.V. Avatar answered Jan 14 '23 14:01

Ole V.V.