Java – I “compact number format” dal JDK 12

Nel framework della piattaforma Standard Edition di Java esiste già da un po’ di tempo la funzionalità dei “compact number format” che era stata introdotta nel JDK 12 rilasciato nel mese di Marzo del 2019. Si tratta di un nuovo modo di formattare in stringa i numeri usando una forma compatta, in una maniera praticamente molto similare a come, ad esempio, un sistema operativo indica in modo abbreviato che un file ha la dimensione di es. 57 MB invece che indicare il numero esatto di byte.

Questa funzionalità è ad oggi ancora poco nota ma probabilmente solo perché è stata poco “pubblicizzata” e poco documentata (non ne ho trovato traccia nemmeno nel tutorial ufficiale Oracle). Questo articolo serve a descrivere questa funzionalità mostrando degli esempi di codice per illustrare il funzionamento in generale.

Nel framework standard di Java esiste da quasi sempre (esattamente dal JDK 1.1) la classe java.text.NumberFormat che gestisce la formattazione (e anche il contrario, ovvero il parsing) dei numeri in modo “localizzato” cioè sfruttando le regole di un java.util.Locale che rappresenta una certa lingua/cultura/paese del mondo. Il formato dei numeri infatti cambia da una localizzazione all’altra, in italiano per separare la parte intera di un numero dalla parte frazionaria si usa la virgola (“,”) mentre invece nella lingua inglese si usa il punto (“.”).

NumberFormat è una classe astratta, per poter fare qualcosa di utile è necessario ottenere innanzitutto la istanza di una implementazione concreta di NumberFormat. Per poter fare questo, NumberFormat mette a disposizione una serie di metodi statici che fungono da factory, tra cui ad esempio:

  • public static NumberFormat getInstance()
  • public static NumberFormat getInstance(Locale inLocale)
  • public static NumberFormat getIntegerInstance()
    ecc...

Nel JDK 12 sono stati introdotti 2 nuovi metodi factory:

  • public static NumberFormat getCompactNumberInstance()

  • public static NumberFormat getCompactNumberInstance(Locale locale, NumberFormat.Style formatStyle)

NumberFormat.Style è una semplice nuova enum che descrive lo stile di formattazione che può essere di due tipi differenti:

  • SHORT, la forma più compatta, per formattare ad esempio in inglese "123K"
  • LONG, la forma estesa, per formattare ad esempio in inglese "123 thousand"

Per usare i nuovi compact number format basta quindi scegliere quale dei due metodi factory utilizzare. La versione base getCompactNumberInstance() usa il Locale predefinito e lo stile SHORT fisso. L’altra versione permette di specificare esplicitamente il Locale e anche lo stile.

Il seguente codice permette di farsi velocemente una idea di come si comporta questa nuova funzionalità.

import java.text.NumberFormat;
import java.util.Locale;

public class ProvaCompactNumberFormat {
    public static void main(String[] args) {
        Locale[] locales = { Locale.ENGLISH, Locale.ITALIAN, Locale.FRENCH };
        long[] values = { 1, 12, 123, 1234, 12345, 123456, 1234567, 12345678, 123456789, 1234567890 };

        for (Locale locale : locales) {
            System.out.printf("Locale: %s (%s)%n", locale, locale.getDisplayName());
            System.out.printf("              SHORT      LONG%n");

            NumberFormat nfShort = NumberFormat.getCompactNumberInstance(locale, NumberFormat.Style.SHORT);
            NumberFormat nfLong = NumberFormat.getCompactNumberInstance(locale, NumberFormat.Style.LONG);

            for (long value : values) {
                System.out.printf("  %-10d  %-10s %-12s%n",
                        value, nfShort.format(value), nfLong.format(value));
            }

            System.out.println();
        }
    }
}

Questo programma di esempio mette semplicemente in combinazione 3 lingue differenti (giusto come esempio ho preso l’inglese, l’italiano e il francese), 10 valori numerici e i due stili di formattazione SHORT e LONG.

Eseguendo il programma, il risultato è il seguente (nota: è stato ottenuto con il JDK Oracle 16.0.1):

Locale: en (inglese)
              SHORT      LONG
  1           1          1           
  12          12         12          
  123         123        123         
  1234        1K         1 thousand  
  12345       12K        12 thousand 
  123456      123K       123 thousand
  1234567     1M         1 million   
  12345678    12M        12 million  
  123456789   123M       123 million 
  1234567890  1B         1 billion   

Locale: it (italiano)
              SHORT      LONG
  1           1          1           
  12          12         12          
  123         123        123         
  1234        1.234      mille       
  12345       12.345     12 mila     
  123456      123.456    123 mila    
  1234567     1 Mln      1 milione   
  12345678    12 Mln     12 milioni  
  123456789   123 Mln    123 milioni 
  1234567890  1 Mrd      1 miliardi  

Locale: fr (francese)
              SHORT      LONG
  1           1          1           
  12          12         12          
  123         123        123         
  1234        1 k        1 millier   
  12345       12 k       12 mille    
  123456      123 k      123 mille   
  1234567     1 M        1 million   
  12345678    12 M       12 millions 
  123456789   123 M      123 millions
  1234567890  1 Md       1 milliard  

Guardando il risultato prodotto dal codice, qualcuno potrebbe obiettare che una formattazione fatta in quel modo in realtà non è particolarmente utile, perché ad esempio il numero 1234567 viene formattato come "1 Mln" / "1 milione" ma così si “perde” tantissimo la precisione del valore. Mentre invece sarebbe decisamente più utile avere es. "1,23 Mln" / "1,23 milione".
Questa osservazione è assolutamente corretta e appropriata ma a questo punto è bene chiarire che il risultato prodotto è solamente dovuto alla configurazione predefinita e si può fare ben di meglio.

Bisogna infatti considerare sempre che l’oggetto che abbiamo in mano è un NumberFormat (sebbene specializzato per i nuovi compact number). La classe NumberFormat possiede 8 metodi specifici che sono:

  • int getMinimumIntegerDigits() / void setMinimumIntegerDigits(int newValue)
  • int getMaximumIntegerDigits() / void setMaximumIntegerDigits(int newValue)
  • int getMinimumFractionDigits() / void setMinimumFractionDigits(int newValue)
  • int getMaximumFractionDigits() / void setMaximumFractionDigits(int newValue)

Questi metodi servono per gestire e controllare in maniera precisa il numero minimo/massimo di cifre che ci possono essere per la parte intera e soprattutto per la parte frazionaria.

Quello che avviene con i NumberFormat nel nuovo stile compact semplicemente è che per default il maximumFractionDigits è impostato a 0 (ho verificato, vale per tutti i Locale supportati, indipendentemente dallo stile SHORT o LONG). Ma questo valore si può ovviamente cambiare! Per verificare questo aspetto basta provare il seguente codice in cui viene usato un Locale fisso e un valore numerico fisso ma si varia di volta in volta il maximumFractionDigits.

import java.text.NumberFormat;
import java.util.Locale;

public class ProvaCompactNumberFormat2 {
    public static void main(String[] args) {
        Locale locale = Locale.ITALIAN;
        long value = 1234567;

        System.out.printf("            SHORT        LONG%n");

        NumberFormat nfShort = NumberFormat.getCompactNumberInstance(locale, NumberFormat.Style.SHORT);
        NumberFormat nfLong = NumberFormat.getCompactNumberInstance(locale, NumberFormat.Style.LONG);

        for (int digits = 0; digits < 5; digits++) {
            nfShort.setMaximumFractionDigits(digits);
            nfLong.setMaximumFractionDigits(digits);

            System.out.printf("  %-9d %-12s %-12s%n",
                    value, nfShort.format(value), nfLong.format(value));
        }
    }
}

Il risultato è il seguente:

            SHORT        LONG
  1234567   1 Mln        1 milione   
  1234567   1,2 Mln      1,2 milione 
  1234567   1,23 Mln     1,23 milione
  1234567   1,235 Mln    1,235 milione
  1234567   1,2346 Mln   1,2346 milione

Andando a modificare il maximumFractionDigits del NumberFormat si può avere il numero di cifre che si vuole per la parte frazionaria, ottenendo quindi un risultato più utile e comprensibile.

Il parsing con i compact number format

Fin dall’inizio dell’articolo si è parlato principalmente solo della formattazione in stringa dei numeri usando questa nuova funzionalità. Ma la domanda potrebbe sicuramente venire in mente: i nuovi compact number format si possono utilizzare anche per il parsing? Ovvero, intepretare una stringa in forma compatta per ottenere un valore numerico.

La risposta è , i compact number format possono essere utilizzati anche per il parsing. Chiaramente, è giusto dirlo anche se è facilmente intuibile, la precisione del numero che si ottiene è uguale alla precisione che possiede la forma abbreviata in stringa. Se la stringa contiene es. "1 thousand" (lingua inglese), si potrà ottenere solo 1000, non qualcosa di più preciso.

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.Locale;

public class ProvaCompactNumberFormat3 {
    public static void main(String[] args) throws ParseException {
        NumberFormat nfLong = NumberFormat.getCompactNumberInstance(Locale.ENGLISH, NumberFormat.Style.LONG);

        String[] strings = { "1 thousand", "1.2 thousand", "1.23 thousand", "1.234 thousand" };

        for (String s : strings) {
            System.out.printf("%-16s %d%n", s, nfLong.parse(s));
        }
    }
}

Il risultato prodotto è il seguente:

1 thousand       1000
1.2 thousand     1200
1.23 thousand    1230
1.234 thousand   1234

Conclusioni

I nuovi “compact number format” sono al momento ancora poco noti e poco sfruttati. Chiaramente ha senso utilizzarli solo quando si deve ottenere una forma molto compatta e la precisione assoluta non è l’aspetto più importante.

L’utilizzo a livello pratico è molto semplice, per riassumere si può dire che questi nuovi compact NumberFormat si possono controllare agendo a 3 livelli differenti:

  1. sulla localizzazione con un java.util.Locale (italiano, inglese, ecc...)
  2. sullo stile di formattazione SHORT oppure LONG
  3. sul numero di cifre minime/massime tramite i metodi citati prima (più ovviamente gli altri di NumberFormat che non ho citato tra cui setGroupingUsed, setRoundingMode e altri.)