I. Introduction▲
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 :
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.
II. Les valeurs par défaut du NumberFormat.getInstance()▲
II-A. 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.
2.
NumberFormat.getInstance
(
Locale.FRENCH).format
(
1.235
d)); // => " 1,235 "
NumberFormat.getInstance
(
Locale.ENGLISH).format
(
1.235
d)); // => " 1.235 "
II-B. 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.
Pour la suite de ce tutoriel, je vous propose de nous concentrer sur le getNumberInstance ou DecimalFormat.
III. 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 €.
III-A. 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.
2.
3.
4.
5.
final
NumberFormat instance =
NumberFormat.getNumberInstance
(
);
instance.setMinimumFractionDigits
(
2
);
instance.setMaximumFractionDigits
(
2
);
assert
"1,11"
.equals
(
instance.format
(
1.111
d));
assert
"1,10"
.equals
(
instance.format
(
1.1
d));
Dans le cas où MaximumFractionDigits< MinimumFractionDigits, l'ordre d'affectation peut changer la donne car c'est le dernier qui a raison :
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.11111
d));
// MinimumFractionDigits override MaximumFractionDigits
instance.setMaximumFractionDigits
(
3
);
instance.setMinimumFractionDigits
(
5
);
assert
"1,11111"
.equals
(
instance.format
(
1.11111
d));
III-B. 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.
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
(
1
D));
assert
"2 345"
.equals
(
instance.format
(
12345
D));
instance.setGroupingUsed
(
false
);
assert
"2345"
.equals
(
instance.format
(
12345
D));
Là encore dans le cas où MaximumIntegerDigits< MinimumIntegerDigits, l'ordre d'affectation peut changer la donne.
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
(
1
D));
instance.setMaximumIntegerDigits
(
1
);
assert
"1"
.equals
(
instance.format
(
1
D));
IV. 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 :
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.564
D));
assert
"1234,57"
.equals
(
instance.format
(
1234.565
D));
Mais si c'était si simple...
2.
assert
"1234,66"
.equals
(
instance.format
(
1234.664
D));
assert
"1234,67"
.equals
(
instance.format
(
1234.665
D)); // => 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.
V. 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.
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.565
f));
assert
"1234,56"
.equals
(
instance.format
(
Float.valueOf
(
1234.565
f)));
Pour éviter ce problème, il suffit d'encapsuler notre nombre dans un BigDecimal qui lui est traité sans conversion en double primitive.
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.564
f).toString
(
))));
assert
"1234,57"
.equals
(
instance.format
(
new
BigDecimal
(
Float.valueOf
(
1234.565
f).toString
(
))));
VI. 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 (cf. https://docs.oracle.com/javase/tutorial/i18n/format/decimalFormat.html). 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 :
2.
3.
final
NumberFormat instance =
NumberFormat.getNumberInstance
(
);
((
DecimalFormat) instance).applyPattern
(
"###.00 €"
);
assert
"1234,50 €"
.equals
(
instance.format
(
1234.5
D));
VII. Remerciements▲
Cet article a été publié avec l'aimable autorisation de la société NetapsysNetapsys.
Nous tenons à remercier KartSeven pour la relecture de cet article et Mickael Baron pour la mise au gabarit.