Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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.

like image 241
Xaerxess Avatar asked Jun 19 '13 09:06

Xaerxess


2 Answers

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.

like image 194
Jon Skeet Avatar answered Sep 22 '22 17:09

Jon Skeet


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()));
like image 43
jarnbjo Avatar answered Sep 22 '22 17:09

jarnbjo