Month name in genitive (Polish locale) with Joda-Time DateTimeFormatter
Asked Answered
S

2

21

I have LocalDate which contains date 2012-12-28 and I want to print it with localized month name (i.e. December in Polish) in genitive which in Polish is distinct from nominative (grudnia and grudzień respectively). Because I also want to use custom format I created my own DateTimeFormatter using DateTimeFormatterBuilder (which AFAIK is the right way to it in Joda-Time):

private static final DateTimeFormatter CUSTOM_DATE_FORMATTER
    = new DateTimeFormatterBuilder()
        .appendLiteral("z dnia ")
        .appendDayOfMonth(1)
        .appendLiteral(' ')
        .appendText(new MonthNameGenitive()) // <--
        .appendLiteral(' ')
        .appendYear(4, 4)
        .appendLiteral(" r.")
        .toFormatter()
        .withLocale(new Locale("pl", "PL")); // not used in this case apparently

The output should be "z dnia 28 grudnia 2012 r.".

My question is about line marked with an arrow: how should I implement MonthNameGenitive? Currently it extends DateTimeFieldType and has quite much code:

final class MonthNameGenitive extends DateTimeFieldType {
  private static final long serialVersionUID = 1L;

  MonthNameGenitive() {
    super("monthNameGenitive");
  }

  @Override
  public DurationFieldType getDurationType() {
    return DurationFieldType.months();
  }

  @Override
  public DurationFieldType getRangeDurationType() {
    return DurationFieldType.years();
  }

  @Override
  public DateTimeField getField(final Chronology chronology) {
    return new MonthNameGenDateTimeField(chronology.monthOfYear());
  }

  private static final class MonthNameGenDateTimeField
      extends DelegatedDateTimeField {
    private static final long serialVersionUID = 1L;
    private static final ImmutableList<String> MONTH_NAMES =
        ImmutableList.of(
            "stycznia", "lutego", "marca", "kwietnia", "maja", "czerwca",
            "lipca", "sierpnia", "września", "października", "listopada",
            "grudnia");

    private MonthNameGenDateTimeField(final DateTimeField field) {
      super(field);
    }

    @Override
    public String getAsText(final ReadablePartial partial,
        final Locale locale) {
      return MONTH_NAMES.get(
          partial.get(this.getType()) - 1); // months are 1-based
    }
  }

}

Seems sloppy and not bullet-proof to me, since I had to implement many magic methods plus I'm using DelegatedDateTimeField and overriding only one method (getAsText(ReadablePartial, Locale)) while there are others with the same name:

  • getAsText(long, Locale)
  • getAsText(long)
  • getAsText(ReadablePartial, int, Locale)
  • getAsText(int, Locale)

Is there better approach to get desired output (using DateTimeFormatter) or my approach correct yet very verbose?

EDIT:

I've tried to achieve the same thing with new JDK8 Time API (which is similar to Joda, based on JSR-310) and it could be done easily:

private static final java.time.format.DateTimeFormatter JDK8_DATE_FORMATTER
    = new java.time.format.DateTimeFormatterBuilder()
        .appendLiteral("z dnia ")
        .appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NORMAL)
        .appendLiteral(' ')
        .appendText(ChronoField.MONTH_OF_YEAR, MONTH_NAMES_GENITIVE) // <--
        .appendLiteral(' ')
        .appendValue(ChronoField.YEAR, 4)
        .appendLiteral(" r.")
        .toFormatter()
        .withLocale(new Locale("pl", "PL"));

where MONTH_NAMES_GENITIVE is Map<Long, String> with custom month names, so it's very easy to use. See DateTimeFormatterBuilder#appendText(TemporalField, Map).

Interestingly, in JDK8 this whole Polish-month-name-genitive play is not necessary because DateFormatSymbols.getInstance(new Locale("pl", "PL")).getMonths() returns month names in genitive by default... While this change is correct for my use case (in Polish, we say "today is the 28th of December 2012" using month names in genitive), it can be tricky in some other cases (we say "it's December 2012" using nominative) and it's backwards incompatible.

Shadshadberry answered 19/6, 2013 at 9:57 Comment(2)
I'm actually slightly surprised that it doesn't work out when to use the genitive and when to use the other (nominative?) version - we certainly try to get that right in Noda Time.Archaism
@JonSkeet It seems that in JDK8 the genitive/nominative version is supported via TextStyle enum constants FULL / FULL_STANDALONE where FULL is default and for Polish it's genitive and FULL_STANDALONE is nominative - see also this XML file with localized names. So .appendText(ChronoField.MONTH_OF_YEAR, TextStyle.FULL_STANDALONE) can be used for standard month name.Chlorinate
A
13

You have my sympathies - the field system in Joda Time is somewhat complicated.

However, I suggest that in this case the simplest approach would actually be to use DateTimeFormatterBuilder.append(DateTimePrinter printer). Note that this approach will only work if you're only interested in printing - if you need to parse as well, life becomes more complicated.

At that point you only need to implement DateTimePrinter, which is relatively straightforward, particularly if you're happy to ignore the Locale as you're only interested in a single culture. You can put all the logic (not that there'll be much of it) in a single method, and make the rest of the methods just delegate to that method. For the overloads which take a long and a DateTimeZone, just construct a DateTime and call toLocalDateTime, at which point you can delegate to the other methods.

EDIT: In fact, one option would be to write an abstract base class, if you knew you'd only care about the local values:

public abstract class SimpleDateTimePrinter implements DateTimePrinter {

    protected abstract String getText(ReadablePartial partial, Locale locale);

    @Override
    public void printTo(StringBuffer buf, long instant, Chronology chrono,
            int displayOffset, DateTimeZone displayZone, Locale locale) {
        DateTime dateTime = new DateTime(instant, chrono.withZone(displayZone));
        String text = getText(dateTime.toLocalDateTime(), locale);
        buf.append(text);
    }

    @Override
    public void printTo(Writer out, long instant, Chronology chrono,
            int displayOffset, DateTimeZone displayZone, Locale locale)
            throws IOException {
        DateTime dateTime = new DateTime(instant, chrono.withZone(displayZone));
        String text = getText(dateTime.toLocalDateTime(), locale);
        out.write(text);
    }

    @Override
    public void printTo(StringBuffer buf, ReadablePartial partial, Locale locale) {
        buf.append(getText(partial, locale));
    }

    @Override
    public void printTo(Writer out, ReadablePartial partial, Locale locale)
            throws IOException {
        out.write(getText(partial, locale));
    }
}

Then you can easily write a concrete subclass which ignores the locale, and just returns the month:

public class PolishGenitiveMonthPrinter extends SimpleDateTimePrinter {

    private static final ImmutableList<String> MONTH_NAMES =
            ImmutableList.of(
                "stycznia", "lutego", "marca", "kwietnia", "maja", "czerwca",
                "lipca", "sierpnia", "września", "października", "listopada",
                "grudnia");

    private static final int MAX_MONTH_LENGTH;

    static {
        int max = 0;
        for (String month : MONTH_NAMES) {
            if (month.length() > max) {
                max = month.length();
            }
        }
        MAX_MONTH_LENGTH = max;
    }

    @Override
    public int estimatePrintedLength() {
        return MAX_MONTH_LENGTH;
    }

    @Override
    protected String getText(ReadablePartial partial, Locale locale) {
        int month = partial.get(DateTimeFieldType.monthOfYear());
        return MONTH_NAMES.get(month - 1);
    }
}

Of course you could do this all in one class, but I'd probably break it out to make the base class more reusable in the future.

Archaism answered 21/6, 2013 at 13:20 Comment(10)
I saw DateTimePrinter interface but it's documented as internal, has note Application users will rarely use this class directly and has no public implementations so I used custom DateTimeFieldType instead. However, your attempt is cleaner and less verbose, so I'll definitely use it. BTW I wonder why there's no SimpleDateTimePrinter in Joda-Time core, since DateTimePrinter is in DateTimeFormatterBuilder public API.Chlorinate
@Xaerxess: I hadn't spotted the "internal" part - but it seems fairly easy to implement correctly, and as you say it appears as a public type. I can understand that it would be rarely used directly by normal app users - but of course that's not the same thing as it being a problem to do so :)Archaism
You're right, it's probably this rare use ;) DateTimeParser is another "internal yet public" interface which appears in Joda's DateTimeFormatterBuilder and, after comparing the builder with new one from JSR-311 (see edited question), I suspect that "internal" classes where put there because formatting API didn't allow some advanced things like from this question. Java 8 Time API seems to fix that problem.Chlorinate
Has anyone ever got append(DateTimePrinter) to work? I seem to be the first one who has tried, and it appends the text twice. I filed a bug with joda-time, but with this kind of timing, who knows if it will ever be fixed...Declarant
@Trejkaz: Well I tested the code in my answer and it was fine, IIRC. I suggest you ask a new question with a minimal reproducible example.Archaism
I have already filed a bug with joda-time with a unit test demonstrating the problem.Declarant
Here's a demo using your code though: gist.github.com/trejkaz/82de1d63319464a8cf3aaa44ec8108a5 - output: "1 styczniastycznia"Declarant
@Trejkaz: Interesting - if you use toFormatter() instead of toPrinter() it works okay for me. I suspect that's how I tested it.Archaism
I get the same result there too, but actually, DateTimeFormatter allows passing an Appendable which is not a StringBuffer or Writer, which bypasses the bug, so it seems like a valid workaround. I'm not sure why toFormatter() works though, because I never provided a parser...Declarant
Ah, I see the trap now. If I use toFormatter() and pass a StringBuilder, now the printTo causes new object allocations, which is what I was trying to avoid by using a StringBuffer. I guess I'll just have to wait for them to fix the bug and live with the code I have today. Or, I guess, write my own date formatter. I will at least investigate ICU's alternative API as maybe they don't have the same problems as the JDK's equivalent classes.Declarant
S
4

Do you really need to use Joda? Replacing the month names is trivial using the date formatters in the standard Java API:

SimpleDateFormat sdf = new SimpleDateFormat("'z dnia' dd MMMM yyyy 'r.'");

DateFormatSymbols dfs = sdf.getDateFormatSymbols();

dfs.setMonths(
    new String[] {
        "stycznia", "lutego", "marca", "kwietnia", "maja", "czerwca",
        "lipca", "sierpnia", "września", "października", "listopada",
        "grudnia"   
    });

sdf.setDateFormatSymbols(dfs);

System.out.println(
   sdf.format(new GregorianCalendar(2012, Calendar.DECEMBER, 28).getTime()));
Saying answered 19/6, 2013 at 10:58 Comment(1)
I'd rather use Joda (company recommendations), plus SimpleDateFormat is not thread safe. Since there's an easy way to change month names in standard JDK Date API, I wonder if there is one in Joda...Chlorinate

© 2022 - 2024 — McMap. All rights reserved.