4

I am trying to format a MonthDay object in a way that I do not have to specify the order. I am trying to use a localized DateTimeFormatter.

I have this code:

LocalDate datetime = LocalDate.parse("2017-08-11", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
MonthDay monthday = MonthDay.from(datetime);
System.out.println(monthday.format(DateTimeFormatter.ofPattern("MMMM dd").withLocale(Locale.ENGLISH)));
System.out.println(monthday.format(DateTimeFormatter.ofPattern("MMMM dd").withLocale(Locale.GERMANY)));
System.out.println(monthday.format(DateTimeFormatter.ofPattern("MMMM dd").withLocale(Locale.forLanguageTag("UK"))));

System.out.println(datetime.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.ENGLISH)));
System.out.println(datetime.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.forLanguageTag("UK"))));
// next line throws exception for java.time.temporal.UnsupportedTemporalTypeException: Unsupported field: YearOfEra
System.out.println(monthday.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.forLanguageTag("UK"))));

The first 3 prints will print as expected the translated Month and day, but it is always month and then day. It does not change the order because I am explicitly telling it the order.

The next two (before the exception) would print respectively:

Aug 11, 2017
11 серп. 2017

Notice how the day is either before or after the month depending on the locale passed to the function. How do I do this with a MonthDay object as the last line throws an exception when done in this way.

Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
KadeYuy
  • 43
  • 1
  • 4

3 Answers3

3

The other answers given so far describe the limitations of the standard DateTimeFormatter, see also the unsolved related JDK-issue. The suggested workaround to edit the localized date pattern by removing "y" etc. is tricky and might not work for all locales due to the existence of other localized literals inside the pattern.

However, you might also consider using external libraries which have a stronger focus on internationalization issues and have the capability to format a month-day-object using just the locale information. So the locale determines the order of field components and also dots, spaces or other special literals (like in Chinese).

Here two options with the necessary type conversions related to your system timezone:

ICU4J

MonthDay md = MonthDay.now();

GregorianCalendar gcal =
    new GregorianCalendar(
        2000, // avoids possible leap year problems
        md.getMonthValue() - 1,
        md.getDayOfMonth()
    );

DateFormat df =
    DateFormat.getInstanceForSkeleton(
        DateFormat.ABBR_MONTH_DAY,
        Locale.forLanguageTag("en")
    );
System.out.println(df.format(gcal.getTime())); // Aug 15

DateFormat df2 =
    DateFormat.getInstanceForSkeleton(
        DateFormat.ABBR_MONTH_DAY,
        Locale.forLanguageTag("de")
    );
System.out.println(df2.format(gcal.getTime())); // 15. Aug.

DateFormat df3 =
    DateFormat.getInstanceForSkeleton(DateFormat.MONTH_DAY, Locale.forLanguageTag("zh"));
System.out.println(df3.format(gcal.getTime())); // 8月15日

Time4J

MonthDay md = MonthDay.now();

ChronoFormatter<AnnualDate> cf1 =
    ChronoFormatter.ofStyle(DisplayMode.SHORT, Locale.GERMAN, AnnualDate.chronology());
System.out.println(cf1.format(AnnualDate.from(md))); // 15.8.

ChronoFormatter<AnnualDate> cf2 =
    ChronoFormatter.ofStyle(DisplayMode.MEDIUM, Locale.GERMAN, AnnualDate.chronology());
System.out.println(cf2.format(AnnualDate.from(md))); // 15.08.

ChronoFormatter<AnnualDate> cf3 =
    ChronoFormatter.ofStyle(DisplayMode.LONG, Locale.ENGLISH, AnnualDate.chronology());
System.out.println(cf3.format(AnnualDate.from(md))); // Aug 15

ChronoFormatter<AnnualDate> cf4 =
    ChronoFormatter.ofStyle(DisplayMode.FULL, Locale.GERMAN, AnnualDate.chronology());
System.out.println(cf4.format(AnnualDate.from(md))); // 15. August

ChronoFormatter<AnnualDate> cf5 =
    ChronoFormatter.ofStyle(DisplayMode.FULL, Locale.CHINESE, AnnualDate.chronology());
System.out.println(cf5.format(AnnualDate.from(md))); // 8月15日

Disclaimer: Time4J has been written by myself to fill gaps or to improve other features of JSR-310 (java.time-package).

Meno Hochschild
  • 42,708
  • 7
  • 104
  • 126
  • When you say "2000, // avoids possible leap year problems" what problems do you mean? Will the calendar increase or decrease the day I give it? – KadeYuy Aug 23 '17 at 00:46
  • @KadeYuy The 2000-issue is about the conversion from a `MonthDay`-instance (which might stand for the 29th of February) to a `GregorianCalendar`. If we choose a non-leap-year (like 2017) then the original 29th of February would be lost (and here silently converted to first of March). – Meno Hochschild Aug 23 '17 at 14:11
  • Ahh I see. So if I use the year from my full date I should be fine as well as long as it is defined. Thank you for the help! – KadeYuy Aug 23 '17 at 23:34
0

MonthDay does not store the year information and the FormatStyle.MEDIUM requires a year value, thats why the formatter is not finding the field YearOfEra same occurs if you use YearMonth with same FormatStyle but now for the missing field DayOfMonth.

MonthDay API

This class does not store or represent a year, time or time-zone. For example, the value "December 3rd" can be stored in a MonthDay.

You could transform monthday to an Instant or simply use the datetime.

dgofactory
  • 348
  • 5
  • 13
-1

ofLocalizedDate returns a formatter for the date, so it formats the day, month and year fields, so the object being formatted needs to have all the three fields. MonthDay doesn't have the year field, that's why it throws a UnsupportedTemporalTypeException.


If you want to print the whole date (with day, month and year), you must add the year to the MonthDay object. You can use the atYear method for that:

System.out.println(monthday.atYear(2017).format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.forLanguageTag("UK"))));

This will output:

11 серп. 2017

If you want the current year, just use Year.now().getValue() instead of 2017.

You can also use datetime.getYear(), if you want the same year of the LocalDate - or use the datetime instead of the monthday.


If you want to print just the day and month, you'll have to do some workarounds.

As the localized formatters are built-in in the JDK (and there seems to be no way to change them), you don't have a direct way of doing it, though there are some alternatives.

One (ugly) solution is to set a year and then remove it from the formatted String:

DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.forLanguageTag("UK"));
// set year to 2017, then remove "2017" from the formatted string
System.out.println(monthday.atYear(2017).format(formatter).replace("2017", "").trim());

The output is:

11 серп.

The boring part is to remove all extra characters that might exist in each locale. For the English locale, I also had to remove the ,:

DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(Locale.ENGLISH);
// remove the year (2017) and the "," from the output
System.out.println(monthday.atYear(2017).format(formatter).replace("2017", "").replace(",", "").trim());

The output is:

Aug 11

You'll have to check all the extra characters (such as ,) for all locales, and remove them from the output. Or just use ofPattern with fixed non-locale specific patterns (as you did in your first 3 tests).

As @BasilBourque's noticed in the comments, I'm assuming that the year is at the beggining or end of the pattern. If the year is in the middle, there will be some extra spaces in the final result, which can be removed with .replaceAll("\\s{2,}", " ") (2 or more spaces are replaced by just one).


Another alternative (as suggested by @JodaStephen comment) is to use a DateTimeFormatterBuilder to get the localized date pattern.

Then I remove the year from the pattern, replacing y and u (the patterns used for the year), and also remove some other characters (like , and extra spaces).

With the resulting pattern (without the year), I create a DateTimeFormatter with the specified locale and format the MonthDay:

// get date pattern for the specified locale
String pattern = DateTimeFormatterBuilder.getLocalizedDateTimePattern(FormatStyle.MEDIUM, null, IsoChronology.INSTANCE, Locale.forLanguageTag("UK"));
pattern = pattern
    // remove the year (1 or more occurrences of "y" or "u")
    .replaceAll("[yu]+", "")
    // replace "," (you can change this to remove any other characters you want)
    .replaceAll(",", "")
    // replace 2 or more spaces with just one space and trim to remove spaces in the start or end
    .replaceAll("\\s{2,}", " ").trim();
// create formatter for the pattern and locale
DateTimeFormatter fmt = DateTimeFormatter.ofPattern(pattern, Locale.forLanguageTag("UK"));
System.out.println(monthday.format(fmt));

The output will be:

11 серп.

Just reminding that this example might be incomplete, because there are some locales that uses /, - and other characters as separators and you must remove them from the final result. Check all the locales you're working with and remove the characters accordingly.

Another corner-case not covered by this is when you have y or u as literals (inside '): in this case they shouldn't be removed. Anyway, you'll have to check the formats for all locales you're working with and handle each case accordingly.

  • (A) Clever, but it seems like you assume the year is at the beginning or the ending. I do not know if any locales would put year in the middle, but perhaps. (B) I vaguely recall another post suggesting getting the string of the formatter's pattern and deleting the `y` or `u` characters – might be another route though I do not recall the details. – Basil Bourque Aug 15 '17 at 02:15
  • 2
    See getLocalizedDateTimePattern https://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatterBuilder.html#getLocalizedDateTimePattern-java.time.format.FormatStyle-java.time.format.FormatStyle-java.time.chrono.Chronology-java.util.Locale- – JodaStephen Aug 15 '17 at 05:39
  • @BasilBourque Indeed, I was assuming the year in the start or end. I've updated the answer (I also found a way to get the pattern and replace `y` and `u`), thanks a lot! –  Aug 15 '17 at 12:15
  • @JodaStephen Great! I was looking for this in `DateTimeFormatter` class and forget to look at `DateTimeFormatterBuilder`. I've updated the answer, thanks a lot! –  Aug 15 '17 at 12:16