Comment formater les nombres en JAVA avec la classe NumberFormat

Temps de lecture : 6 minutes


Les doubles, les float, les int, les long ou les BigDecimal sont autant de manières différentes de stocker un nombre.

Mais au moment de la présentation sur une page web ou un document généré, il est nécessaire de formater ce nombre en chaîne de caractères. L’API java nous fournit pour cela le NumberFormat et ses différentes déclinaisons.

Mais si l’API propose de faire la chose suivante :

1
NumberFormat.getInstance().format(monNombre);

la résolution par défaut est loin d’être évidente et c’est donc avec précaution et paramétrage qu’il faut manier cette API.

Les valeurs par défaut du NumberFormat.getInstance()

La locale

La première chose à savoir est que par défaut, le NumberFormat.getInstance()  utilise la locale par défaut de type FORMAT qui peut être sensiblement différente de la locale par défaut de type DISPLAY en fonction du paramétrage du système. Cette locale permettra notamment d’afficher le chiffre avec une ‘,’ pour une locale FRENCH, ou un ‘.’ pour une locale ENGLISH.

1
2
NumberFormat.getInstance(Locale.FRENCH).format(1.235d)); // => " 1,235 "
NumberFormat.getInstance(Locale.ENGLISH).format(1.235d)); // => " 1.235 "

Le type de format

La seconde grande configuration par défaut du NumberFormat.getInstance()  est qu’il retourne un DecimalFormat. Donc par défaut, le NumberFormat prévoit de vous retourner un formatage de votre nombre en non entier.

Pour choisir spécifiquement le type de format de retour que vous souhaitez avoir, NumberFormat prévoit des méthodes d’instanciations différentes, toutes avec leur variante paramétrant la locale.

Premier constat, il y a une configuration par défaut en fonction du type de formateur utilisé, du nombre de chiffres après la virgule (3 pour le NumberInstance et 2 pour le CurrencyInstance). Mais ceci n’est que la partie émergée de l’iceberg.

Les paramétrages du DecimalFormat

À titre d’exemple dans cette section, nous essaierons d’afficher un montant au centime près avec comme exemple : 1234,56 €.

Paramétrage des fractions

Comme vu précédemment, par défaut le DecimalFormat formate à 3 chiffres après la virgule. Heureusement l’API propose d’autres paramètres :

  • MaximumFractionDigits : qui propose de limiter l’affichage au nième chiffre après la virgule. 3 par défaut. Dans notre cas il nous faut 2 ;
  • MinimumFractionDigits : qui propose de combler l’affichage au nième chiffre après la virgule par des zéros. 0 par défaut. Dans notre cas il nous faut 2.
1
2
3
4
5
final NumberFormat instance = NumberFormat.getNumberInstance();
instance.setMinimumFractionDigits(2);
instance.setMaximumFractionDigits(2);
assert "1,11".equals(instance.format(1.111d));
assert "1,10".equals(instance.format(1.1d));

Dans le cas où MaximumFractionDigits< MinimumFractionDigits, l’ordre d’affectation peut changer la donne car c’est le dernier qui a raison :

1
2
3
4
5
6
7
8
9
// MaximumFractionDigits override MinimumFractionDigits
NumberFormat instance = NumberFormat.getNumberInstance();
instance.setMinimumFractionDigits(5);
instance.setMaximumFractionDigits(3);
assert "1,111".equals(instance.format(1.11111d));
// MinimumFractionDigits override MaximumFractionDigits
instance.setMaximumFractionDigits(3);
instance.setMinimumFractionDigits(5);
assert "1,11111".equals(instance.format(1.11111d));

Paramétrage des entiers

À l’instar des fractions, on va retrouver un paramétrage similaire du nombre d’entier, ainsi qu’un petit paramétrage de mise en forme des milliers.

  • MaximumIntegerDigits : qui propose de limiter l’affichage au nième entier. 40 par défaut ;
  • MinimumIntegerDigits : qui propose de combler l’affichage au nième entier. 1 par défaut ;
  • GroupingUsed : qui définit si l’on groupe les entiers par trinôme ou non. False par défaut.

Dans notre cas il faut empêcher le groupement.

1
2
3
4
5
6
7
8
final NumberFormat instance = NumberFormat.getNumberInstance();
instance.setMaximumIntegerDigits(4);
instance.setMinimumIntegerDigits(4);
instance.setGroupingUsed(true);
assert "0 001".equals(instance.format(1D));
assert "2 345".equals(instance.format(12345D));
instance.setGroupingUsed(false);
assert "2345".equals(instance.format(12345D));

Là encore dans le cas où MaximumIntegerDigits< MinimumIntegerDigits, l’ordre d’affectation peut changer la donne.

1
2
3
4
5
6
7
final NumberFormat instance = NumberFormat.getNumberInstance();
instance.setMaximumIntegerDigits(1);
instance.setMinimumIntegerDigits(4);
instance.setGroupingUsed(true);
assert "0 001".equals(instance.format(1D));
instance.setMaximumIntegerDigits(1);
assert "1".equals(instance.format(1D));

Le type d’arrondi

L’arrondi est une question qui se pose souvent et qui est loin d’être triviale.

Travaillant sur des décimales, nous réalisons un test unitaire de notre méthode de formatage validant le type d’arrondi en fonction de valeurs critiques, en combinant les paramétrages précédents :

1
2
3
4
5
6
7
8
final NumberFormat instance = NumberFormat.getNumberInstance();
instance.setMinimumFractionDigits(2);
instance.setMaximumFractionDigits(2);
// MaximumIntegerDigits - valeur par défaut
// MinimumIntegerDigits - valeur par défaut
instance.setGroupingUsed(false);
assert "1234,56".equals(instance.format(1234.564D));
assert "1234,57".equals(instance.format(1234.565D));

Mais si c’était si simple…

1
2
assert "1234,66".equals(instance.format(1234.664D));
assert "1234,67".equals(instance.format(1234.665D)); // => ERREUR : "1234,66"

Il faut là consulter l’implémentation de DecimalFormat pour se rendre compte que par défaut, le DecimalFormat applique une règle d’arrondi un peu particulière : le HALF_EVEN. Il s’agit d’une règle d’arrondi qui vise à lisser les erreurs d’arrondi lors du maniement d’un grand nombre de chiffres en arrondissant à l’inférieur si le dernier chiffre avant l’arrondi est pair, ou au supérieur s’il est impair.

En gros : 1,5 => 2 et 2,5 => 2.

Heureusement, l’API permet de modifier le mode d’arrondi sur le DecimalFormat en paramétrant le RoundingMode avec l’une des valeurs suivantes :

  • UP : arrondit systématiquement au nombre supérieur par rapport à 0 : 1,1 => 2 ; -1,1 => -2 ;
  • DOWN : arrondit systématiquement au nombre inférieur par rapport à 0 : 1,9 => 1 ; -1,9 => -1 ;
  • CEILING : arrondit systématiquement au nombre supérieur par rapport à l’infini positif : 1,1 => 2 ; -1,9 => -1 ;
  • FLOOR : arrondit systématiquement au nombre inférieur par rapport à l’infini négatif : 1,9 => 1 ; -1,9 => -1 ;
  • HALF_UP : arrondit au plus proche. Dans le cas d’une équidistance (0,5), arrondit au nombre supérieur par rapport à 0 : 1,4 => 1 ; 1.5 => 2 ; -1.4 => -1 ; -1.5 => -2 ;
  • HALF_DOWN : arrondit au plus proche. Dans le cas d’une équidistance (0,5), arrondit au nombre inférieur par rapport à 0 : 1,6 => 2 ; 1.5 => 1 ; -1.6 => -2 ; -1.5 => -1 ;
  • HALF_EVEN : arrondit au plus proche. Dans le cas d’une équidistance (0,5), se comporte comme un HALF_UP si le chiffre à gauche de la fraction retirée est impair et comme un HALF_DOWN s’il est pair : 1,5 => 2 ; 2.5 => 2 ; -1.5 => -2 ; -2.5 => -2 ;
  • UNNECESSARY : retourne une ArithmeticException si le chiffre nécessite un arrondi.

Dans notre cas nous souhaitons arrondir toujours à la moitié supérieure et utilisons donc le paramètre HALF_UP.

Le cast en double sur les float

Si nous avons pu voir qu’à partir d’un double nous arrivions à nos fins, un dernier petit grain de sable vient s’ajouter quand on réalise un format à partir d’un float ou d’un Float.

Dans le cas de la primitive float, c’est java qui cast automatiquement notre nombre en double pour l’appel de la méthode avec la signature : public final String format(double) ; Dans le cas de l’objet Float, c’est l’API qui lors de l’appel de la méthode avec la signature public final String format(Object), réalise la transformation par un ((Number)myFloat).doubleValue(). Le problème c’est que lors de ce cast forcé, 1234.565F devient 1234.56494140625D, qui dans notre exemple est alors arrondi au chiffre inférieur.

1
2
3
4
5
6
7
8
9
final NumberFormat instance = NumberFormat.getNumberInstance();
instance.setMinimumFractionDigits(2);
instance.setMaximumFractionDigits(2);
// MaximumIntegerDigits - valeur par défaut
// MinimumIntegerDigits - valeur par défaut
instance.setGroupingUsed(false);
instance.setRoundingMode(RoundingMode.HALF_UP);
assert "1234,56".equals(instance.format(1234.565f));
assert "1234,56".equals(instance.format(Float.valueOf(1234.565f)));

Pour éviter ce problème, il suffit d’encapsuler notre nombre dans un BigDecimal qui lui est traité sans conversion en double primitive.

1
2
3
4
5
6
7
8
9
final NumberFormat instance = NumberFormat.getNumberInstance();
instance.setMinimumFractionDigits(2);
instance.setMaximumFractionDigits(2);
// MaximumIntegerDigits - valeur par défaut
// MinimumIntegerDigits - valeur par défaut
instance.setGroupingUsed(false);
instance.setRoundingMode(RoundingMode.HALF_UP);
assert "1234,56".equals(instance.format(new BigDecimal(Float.valueOf(1234.564f).toString())));
assert "1234,57".equals(instance.format(new BigDecimal(Float.valueOf(1234.565f).toString())));

La possibilité de préciser le pattern

Enfin, le DecimalFormat permet de définir le pattern utilisé si l’on souhaite par exemple ajouter un symbole monétaire. Si le constructeur du DecimalFormat accepte dans sa signature le String pattern à utiliser, le NumberFormat.getNumberInstance() ne nous le permet pas. Mais il est possible de faire comme suit :

1
2
3
final NumberFormat instance = NumberFormat.getNumberInstance();
((DecimalFormat) instance).applyPattern("###.00 €");
assert "1234,50 €".equals(instance.format(1234.5D));
comments powered by Disqus

Articles Similaires

Ubuntu 24.04 LTS - Une version qui fait débat entre déception et enthousiasme

Ubuntu 24.04 LTS, “Noble Numbat”, a récemment été déployée, apportant son lot de nouveautés et de changements. Cette version suscite à la fois de l’enthousiasme et de la déception au sein de la communauté des utilisateurs et des développeurs. Déception et colère face à la gestion des paquets DEB Plusieurs utilisateur d’Ubuntu ont exprimé leur déception et colère face à la décision de Canonical, la société mère d’ Ubuntu, de favoriser les paquets Snap au détriment des paquets DEB.

Lire la Suite

Le concours de beauté Miss AI : un cauchemar dystopique ou le futur de la beauté ?

Dans un monde où la technologie et la beauté fusionnent, le concours de beauté Miss AI fait son apparition. Ce concours, organisé par The World AI Creator Awards, récompense les créateurs d’images et d’influenceurs générés par intelligence artificielle (IA). Mais qu’est-ce que cela signifie pour les standards de beauté et les femmes ? Le concours Miss AI est ouvert aux créateurs d’images et d’influenceurs générés par IA qui souhaitent montrer leur charme et leur compétence technique.

Lire la Suite

Le gouvernement du Salvador prend un coup dur : les hackers divulguent le code source et les accès VPN du portefeuille bitcoin national Chivo !

Le programme bitcoin du gouvernement du Salvador, Chivo, a été victime d’une série d’attaques informatiques ces derniers jours. Les hackers ont déjà divulgué les données personnelles de plus de 5 millions de Salvadoriens. Maintenant, les mêmes pirates informatiques ont publié des extraits du code source et des informations d’accès VPN du portefeuille bitcoin national Chivo sur un forum de hacking en ligne, CiberInteligenciaSV. Ceci est un coup dur pour El Salvador, qui lutte pour être un pionnier dans l’adoption du bitcoin.

Lire la Suite