Hakanai
Hakanai

Reputation: 12680

How can I customise the number format for Joda Time date formatting?

I would like to format dates, while formatting the numbers in the date in dozenal.

With the older Java date formatting API, I am able to do this:

    format = new SimpleDateFormat(pattern, new DateFormatSymbolsAdapter(locale)) {{
        numberFormat = new DozenalNumberFormat();
    }};

Unfortunately, SimpleDateFormat internally has some dumb code which is allocating objects too rapidly for my purposes, as I'm formatting values rather frequently. Joda Time might not have this problem (their other classes seem to be fine so far), so I'm attempting to switch.

However, with Joda Time's date formatting classes, it isn't entirely clear how I can do it. A lot of the API is locked down in a way which makes it hard to get in and do what I want:

Surely there has to be some way to do it though. Does anyone have any idea? I think that perhaps someone has had to do it to get Arabic date formatting to work correctly in the past as well, as I vaguely recall Joda Time having problems with that, but maybe that is in the past. I might be the first one trying to do it because I want a different number base for the numbers...

Upvotes: 4

Views: 907

Answers (2)

Meno Hochschild
Meno Hochschild

Reputation: 44061

Joda-Time is obviously not capable of printing other digits than ASCII-digits 0-9. I have investigated all relevant parts of Joda-documentation, even the classes DateTimeUtils and FormatUtils. And setting the locale to a locale which mandates the usage of an alternative numbering system does not help, too.

String s = DateTimeFormat.forPattern("yyyy-MM-dd").withLocale(new Locale("ar"))
  .print(System.currentTimeMillis()));
// output: 2017-01-02 (still ASCII)

The newest CLDR-data (version v30.02) tell us that Arabic uses the alternative numbering system with code name "arab" (xml-tag defaultNumberingSystem). However, the JDK might not be always up-to-date. Often the JDK relies on an old CLDR-version. But even then, as far as I remember, also old CLDR-versions didn't use ASCII-digits for Arabic.

Conclusion: You should not use Joda-Time for serious i18n-work (one among many other details like fixed start of week etc. where this library is notoriously bad). If you still insist on using Joda-Time then you can go the hard way to write your own customized DateTimePrinter. But this is not fun as you have also noticed in your Joda-issue (and will still be no fun after possible fix because it is sooo awkward).

So let's look at better alternatives.


Java-8

    Locale loc = new Locale("ar");
    System.out.println(DateTimeFormatter.ofPattern("yyyy-MM-dd")
    .withDecimalStyle(DecimalStyle.of(loc))
    .format(LocalDate.now()));
    // output: 2017-01-02 (obviously my JDK uses wrong or outdated data)

    System.out.println(DateTimeFormatter.ofPattern("yyyy-MM-dd")
    .withDecimalStyle(DecimalStyle.STANDARD.withZeroDigit('\u0660'))
    .format(LocalDate.now()));
    // correct output with hardwired numbering system

So we see that using the standard on Java-8 is better than Joda-Time but still not without quirks. The correct and only half-way-flexible solution makes usage of class DecimalStyle.

My library Time4J (also runnable on Java-6 with version line v3.x):

I have written an alternative format and parse engine which can also process Java-8-types like LocalDate, Instant etc. Time4J has its own repository for localized resources independent from JDK and actually uses CLDR-version v30.0.2. Showing two ways, either a generic way by locale or using hardwired assumption about numbering system:

System.out.println(
    ChronoFormatter.ofPattern(
        "yyyy-MM-dd",
        PatternType.CLDR,
        new Locale("ar"),
        PlainDate.axis(TemporalType.LOCAL_DATE)
    ).format(LocalDate.now()));

System.out.println(
    ChronoFormatter.ofPattern(
        "yyyy-MM-dd",
        PatternType.CLDR,
        Locale.ROOT,
        PlainDate.axis(TemporalType.LOCAL_DATE)
    )
    .with(Attributes.NUMBER_SYSTEM, NumberSystem.ARABIC_INDIC)
    .format(LocalDate.now()));

Both ways produces digit representations based on zero digit ٠ (unicode point 0660). The year 2017 is displayed as: ٢٠١٧


Update:

Your last comments made clear that you are mainly concerned about how to realize the dozenal numbering system (positional number system for base 12). Well, with Java-8, there is not enough flexibility (no DateTimePrinter-interface like in Joda-Time, also no more flexible hook than DecimalStyle which only allows to set the zero decimal digit while dozenals are not decimal). In order to fill the gap (and it was not so much work), I have decided to implement the dozenal system within the newest version v3.27 (or v4.23 on Java-8-platforms). For Android, I have just now released Time4A-v3.27-2016j. Example of usage and final solution:

@Test
public void printDate() {
    ChronoFormatter<PlainDate> f =
        ChronoFormatter.setUp(PlainDate.axis(), Locale.ROOT)
            .addFixedInteger(PlainDate.YEAR, 4)
            .addLiteral('-')
            .padNext(2)
            .addInteger(PlainDate.MONTH_AS_NUMBER, 1, 2)
            .addLiteral('-')
            .padNext(2)
            .addInteger(PlainDate.DAY_OF_MONTH, 1, 2)
            .build()
            .with(Attributes.NUMBER_SYSTEM, NumberSystem.DOZENAL)
            .with(Attributes.PAD_CHAR, '0');
    assertThat(
        f.format(PlainDate.of(2017, 10, 11)),
        is("1201-0\u218A-0\u218B"));
}

If you are working on Android then you might also consider to choose a slightly changed code using the old type java.util.Date for interoperability with legacy code, for example with the expression

ChronoFormatter.setUp(
  Moment.axis(TemporalType.JAVA_UTIL_DATE), Locale.getDefault())...

Remark: This solution also makes best efforts to avoid extra array allocation when printing numbers (for example even avoiding Integer.toString() in many cases), especially if you use a StringBuilder as second parameter to the method ´ChronoFormatter.formatToBuffer()`. The overall performance effort so far done is unusally high compared with other libraries.

Upvotes: 2

Basil Bourque
Basil Bourque

Reputation: 338684

Your Question is not at all clear as you do not actually specify what you are trying to do nor exactly what is stopping you. But I will provide a bit of info.

FYI, the Joda-Time project is now in maintenance mode, with the team advising migration to the java.time classes.

Using java.time

Both java.time and Joda-Time use immutable objects. That means you get thread-safety built-in. So, you can cache your formatter object(s) and re-use them. No need to instantiate repeatedly.

In java.time, the java.time.format.DateTimeFormatter class can automatically localize for you. Both the human language and formatting can be automatically assigned from a Locale. See this list of supported locales in Java 8. I suggest using these auto-localized formats when they suffice.

Capture the current moment in UTC. Adjust into a time zone. Note that time zone has nothing to do with locale. You may want to see the wall-clock time in Québec while formatting for presentation to a Moroccan user reading Arabic.

Instant instant = Instant.now ();
ZonedDateTime zdt = instant.atZone ( ZoneId.of ( "America/Montreal" ) );

Generate a String for presentation to the user.

Locale l = new Locale ( "ar" , "MA" ); // Arabic language, cultural norms of Morocco.
DateTimeFormatter f = DateTimeFormatter.ofLocalizedDateTime ( FormatStyle.FULL ).withLocale ( l );

Keep a reference to f to cache that object. No need to instantiate again.

Dump to console.

String output = zdt.format ( f );
System.out.println ( "zdt.toString(): " + zdt );
System.out.println ( "output: " + output );

zdt.toString(): 2017-01-01T15:06:34.255-05:00[America/Montreal]

output: 01 يناير, 2017 EST 03:06:34 م

I am not sure if Arabic will copy-paste correctly here. See live code in IdeOne.com.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Upvotes: 1

Related Questions