Pendant ma dernière formation, le sujet est encore une fois tombé sur les calculs et les erreurs d’arrondis. C’est une discussion que je me rappelle avoir eu pour la première fois il y a bientôt 20 ans chez un client et qui revient régulièrement depuis. Pour résumer très simplement, il suffit de faire le test suivant avec votre langage préféré (ici, c’était une formation Java EE) :
System.out.println(1.0f - 0.9f == 0.1f);
Ce qui donne false comme tout le monde devrait le savoir.
Evidemment, le pro .Net de l’assistance n’a pu s’empêcher de sourire, un peu moins lorsqu’il a fait le test avec son langage préféré.
Il est facile de comprendre ce résultat en effectuant :
System.out.println(1.0f - 0.9f - 0.1f);
On obtient 2.2351742E-8
Passer à du double précision ne résout pas le problème mais le repousse un peu plus loin :
System.out.println(1.0 - 0.9 - 0.1);
on obtient -2.7755575615628914E-17, ce qui est plus proche de 0 mais pas exactement 0.
Ce résultat surprenant est indépendant des langages car il provient de la norme d’encodage des nombres flottants, à savoir la fameuse norme IEEE (IEEE 754 de son vrai nom)
Si votre langage préféré ne produit pas ce résultat surprenant alors il faut vous inquiéter (c’est qu’il ne respecte pas la norme)
Mais pourquoi ce résultat ?
Brièvement, il faut comprendre que la représentation des nombres flottants dans 32 ou 64 bits est basée sur un encodage avec un nombre limité de bits (1 bit de signe, quelques bits pour l’exposant et le reste pour la mantisse). Dans ce format, la représentation de nombre décimaux tels que 0.9 ou 0.1 est tronquée. C’est un peu similaire à la représentation de 1/3 en notation décimale. Le représenter sous la forme 0,33333… est acceptable mais si on tronque, on enlève un petit bout et des additions comme 0,33333+0,33333+0,33333 donnent 0,99999 et non 1.
Il faut retenir que les nombres flottants ne conviennent pas aux calculs monétaires.
Il y a presque 20 ans, le discours avait alors porté sur COBOL.
Ah ah, c’est là qu’on m’avait expliqué qu’en COBOL, les calculs monétaires ne sont pas fait avec des nombres flottants mais avec des entiers, le point décimal étant simplement affiché devant les deux derniers chiffres.
Java propose la classe BigDecimal pour traiter ce problème.
J’ai alors montré à mes stagiaires ce qu’il faudrait normalement écrire :
BigDecimal n1 = new BigDecimal("1.0");
BigDecimal n2 = new BigDecimal("0.9");
BigDecimal n3 = new BigDecimal("0.1");
System.out.println(n1.subtract(n2).subtract(n3));
Et on obtient le 0.0 attendu.
Un dernier test sur notre équation initiale :
System.out.println(n1.subtract(n2) == n3);
Ah !?, pas de chance, ça donne encore false
(l’autoboxing ne résout pas tout)
System.out.println(n1.subtract(n2).equals(n3));
Enfin, on obtient le true attendu.
Là, évidemment, le pro .Net de l’assistance rigole à nouveau. Faut reconnaître que c’est un peu capilo-tracté tout ça.
Il recherche et me montre alors comment cela s’écrirait en .Net.
Je ne me souviens plus exactement comment c’était mais effectivement c’était un petit plus simple.
A ce stade, je suppose que mes collègues et lecteurs assidus se disent « Tiens c’est bizarre, il a pas encore parlé de Smalltalk ! ».
Bon ok, j’y viens (forcément, je cherche toujours à parler de mon langage préféré à moi).
Donc je regarde le pro .Net bien dans les yeux et lui dit un truc du genre « Vous pensez vraiment que ça c’est simple ? »
C’était l’occasion pour moi de montrer c’est quoi la simplicité (et le pur objet, la dynamicité et tout ce qui va avec).
Je lance mon implémentation Smalltalk de prédilection (VisualWorks) et je montre dans un workspace (une fenêtre de texte).
En simple précision on écrit :
1.0 - 0.9 = 0.1
Ce qui donne false (normal, on respecte le standard)
1.0 - 0.9 - 0.1
Donne 2.23517e-8 (toujours normal)
Pour passer en double précision, on écrit :
1.0d - 0.9d - 0.1d
Ce qui donne -2.7755575615629d-17
Oui, c’est bizarre ce « d », mais ça s’explique. En Smalltalk (un langage qui date d’une époque où les octets de mémoire coûtaient cher), les seules nombres flottants qui existaient étaient sur 32 bits. Le langage a ensuite intégré les doubles précisions lorsque ce fut nécessaire.
Java est né après, lorsque la double précision était devenue la norme et que le float était là pour compatibilité).
En Smalltalk, on dispose aussi de nombres de types Fraction. C’est sympa les fractions et ça rappelle l’école (nostalgie, quand tu nous tiens)
1 - (9/10)
Donne (1/10) (trop la classe !)
Et on peut écrire :
1 - (9/10) = (1/10)
Ce qui donne true (c’est y pas beau ça madame !)
Evidemment, les fractions pour la monnaie c’est pas terrible. Du coup, il y a aussi les FixedPoint (initialement nommés ScalableNumber). Ces nombres sont directement exprimables dans la syntaxe avec un suffixe « s ».
Le nombre 1 avec deux décimales peut être noté 1.00s ou bien 1s2.
On peut alors écrire :
1.00s - 0.90s - 0.10s
et on obtient 0.00s
1.00s - 0.90s = 0.10s
Nous donne true.
On peut difficilement faire plus simple.
Ce que je ne comprend pas, c’est pourquoi les langages récents ne proposent pas ce type de nombre directement dans la syntaxe comme le fait VisualWorks depuis au moins 1995 (je ne me souviens plus dans quelle version cela a été introduit).
Manifestement, on doit juger que c’est plus important de laisser les développeurs retomber systématiquement dans les mêmes problèmes de propagation d’erreurs avec les virgules flottantes.