4

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:

  • DateTimeFormatterBuilder methods don't give me a way to specify it
  • DateTimeFormatterBuilder has no arbitrary append that I can see
  • DateTimeFormatter doesn't seem to have any obvious intercept points
  • all the useful stuff seems to be locked inside InternalPrinter and its implementations, which are package private
  • Methods to append DateTimeFormatter, DateTimePrinter and the like appear to require a lot of framework just to make a custom implementation, and I haven't yet made my implementation work. After some investigation, it seems like this is a bug in Joda Time.

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...

Hakanai
  • 12,010
  • 10
  • 62
  • 132
  • 1
    You do not explain what it is you are trying to do. You do not show us `pattern` nor `DateFormatSymbolsAdapter` nor `DozenalNumberFormat`. – Basil Bourque Jan 01 '17 at 19:41
  • Are you trying to get the Eastern Arabic numerals rather than the Westernized digits, as [discussed in Wikipedia](https://en.wikipedia.org/wiki/Arabic_numerals) and as [shown here](https://upload.wikimedia.org/wikipedia/commons/d/d8/EgyptphoneKeypad.jpg)? – Basil Bourque Jan 01 '17 at 21:26
  • @BasilBourque Yes, the last statement of OP gives the answer: "I want a different number base for the numbers..." So the question is finally clear enough and explicitly mentions Arabic date formatting, too. – Meno Hochschild Jan 02 '17 at 11:29
  • 1
    The name DozenalNumberFormat also gives a pretty good hint to what I'm doing. I'm formatting the numbers into dozenal. – Hakanai Jan 03 '17 at 02:42
  • And of course, the mentioning of Arabic was a side-note, only relevant to the question because it occurred to me that the same facility would be necessary to format Arabic dates correctly too. – Hakanai Jan 03 '17 at 04:40

2 Answers2

2

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.

Meno Hochschild
  • 42,708
  • 7
  • 104
  • 126
  • Time4J will definitely get investigated. :) Java 8 is sadly not an option right now, because other outside factors force me to use Java 7. Although, I could potentially use some kind of backport of the java.time stuff from the era where it hadn't become Java 8 yet, if it exists. It sounds like they have fixed a lot of bad things about Joda Time. I also investigated ICU4J, and found it to be even worse with trash object allocation than the built-in DateFormat class in Java was, so that was a dead-end too. – Hakanai Jan 03 '17 at 02:45
  • The Java 8 DateTimeFormatter appears to be lacking a way to set a custom NumberFormat too. A really crappy but maybe viable solution: I could use the appendText overload which takes a Map parameter, and pass in a map covering all possible values. :) Although the Java 8 DateTimeFormatterBuilder is final, which complicates how I would do this. – Hakanai Jan 03 '17 at 04:16
  • For Time4J, NumberSystem appears to be the "how to convert a number to a string" strategy, but because it's an enum, it is not possible to provide an implementation which returns the right string for me. (Well, not without forking Time4J anyway.) – Hakanai Jan 03 '17 at 04:42
  • 1
    @Trejkaz Just to note, if you really need extra flexibility in Time4J then you could use the `ChronoPrinter`-interface whose function is similar to what Joda-Time-`DateTimePrinter` yields. Side-note: Java-8 (and its backport, too) does not have such a generic interface. – Meno Hochschild Jan 03 '17 at 08:21
  • ChronoPrinter is probably what I will look at next, because it lets me format directly to an Appendable without having to create a new String for each value. – Hakanai Jan 04 '17 at 00:25
1

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.

Community
  • 1
  • 1
Basil Bourque
  • 303,325
  • 100
  • 852
  • 1,154
  • Ah, this answers the side-question I raised on the comment on the other answer, in that there is a java 7 backport of the newer API. That might be the way to go then. Assuming of course that they don't proliferate trash creation (the issue I'm trying to dodge is, at the end of the day, SimpleDateFormat allocating multiple FieldPosition objects each time it formats a date, which is stressing the GC on a slow device) – Hakanai Jan 03 '17 at 03:09
  • After a bit of investigation, I can't figure out how to override the NumberFormat for a DateTimeFormatter using Java 8. Other than, of course, passing in a map of all conceivable values. Haha, actually, there's an idea. Pass in a Map of all conceivable values, but initially have none of the values computed and compute the values on the fly as they ask for them. ;) There is still the problem of how to otherwise retain the correct date format for the user's locale though, because in Java 8 they have made the DateTimeFormatterBuilder final, which prevents the obvious workaround for that one. – Hakanai Jan 03 '17 at 04:45
  • @Trejkaz Please edit your Question to specify exactly what you are trying to accomplish. **Reread your own question** and comments… you never describe the actual core problem or goal. The closest you get is “I want a different number base for the numbers” and I have no idea what that means. – Basil Bourque Jan 03 '17 at 07:00
  • Should I have said "radix" instead of "base"? I thought that people were generally aware of what bases were, and it's the less technical term, so I figured it would have been easier to understand. In any case, I updated the first sentence to more precisely say what I'm trying to do. – Hakanai Jan 03 '17 at 22:41
  • @Trejkaz Yes, you should *definitely* explain your purpose more clearly in the Question, and give examples of inputs & outputs. Altering the [radix/base](https://en.wikipedia.org/wiki/Radix) of a number in a string representing a date-time is *highly* unusual, to say the least. – Basil Bourque Jan 03 '17 at 22:43
  • Yes, I agree that it is unusual. :) But nonetheless, currently working okay using SimpleDateFormat, just creating a bit more garbage than I would like. The first sentence of my question should now provide enough information without adding any additional noise. I mean, you could then ask, "why do you want to do that?", but if you keep asking that question, you eventually get back to the philosophical problem of why mankind does anything, and I'm afraid I don't have the answer to that one. – Hakanai Jan 03 '17 at 23:50