5
minutes
Mis à jour le
31/7/2024


Share this post

Découvrez pourquoi le type number en JavaScript ne convient pas pour gérer l'argent. Explorez des alternatives comme Decimal.js et Big.js, et apprenez des leaders du secteur. Conseils pour une gestion précise et fiable de l'argent.

#
Javascript
#
FinTech
Benjamin Renoux
Software Engineer

Quand on parle argent, pensez type de données

Le point de départ de cet article est la réalisation que l'argent n'est pas simplement un nombre basique comme ceux que vous utilisez pour compter les pommes dans votre panier. Que se passe-t-il lorsque vous essayez de multiplier 10€ par 10$ ? Pas facile, hein… Vous avez déjà trouvé miraculeusement 1,546€ dans la poche d'une vieille veste ? Oui, je sais, ce n'est pas vraiment possible non plus. Ces exemples un peu absurdes illustrent le fait que l'argent a ses propres règles et ne peut pas être modélisé simplement par un nombre. Je vous rassure, je ne suis pas le premier à l'avoir réalisé (et peut-être que vous l’avez compris bien avant moi). En 2002, le programmeur Martin Fowler a proposé dans les Patterns of Enterprise Application Architecture un moyen de représenter l'argent, avec des attributs spécifiques et des règles sur les opérations que l’on peut lui appliquer. Pour lui, les deux attributs minimaux nécessaires pour modéliser un type de données monétaire étaient :

Cette représentation très basique sera notre point de départ pour construire un modèle monétaire simple mais robuste.


Comment représenter les montants monétaires ? 

Un montant monétaire est définitivement un nombre particulier : il a une précision fixe (encore une fois, vous ne pouvez pas avoir 4,376€ dans votre poche). Il faut donc choisir une façon de le représenter qui respecte cette contrainte.

Une approche naïve, le type de données number natif de JavaScript

Attention spoiler, ce n'est définitivement pas une bonne idée si vous ne voulez pas voir quelques centimes (si ce n'est euros) disparaître dans le sombre monde de la représentation des nombres flottants.

Une erreur de précision coûteuse

Si vous avez un peu d'expérience avec JavaScript, vous savez déjà que même le calcul le plus simple peut entraîner une erreur de précision que vous n'attendiez pas au départ. L'exemple le plus évident et le plus connu pour mettre en évidence ce phénomène est :

⚡ Essaye par toi-même

Si cet exemple ne vous a pas totalement convaincu, je vous conseille de jeter un œil à cet article qui présente de manière plus exhaustive tous les résultats de calcul problématiques que vous pouvez rencontrer en travaillant avec le type number natif de JavaScript…

Cette légère différence dans les résultats peut vous sembler inoffensive (avec un ordre de grandeur d'environ 10^-16), cependant, dans une application financière critique, une telle erreur peut se propager rapidement. Il suffit de considérer des transfert de fonds entre des milliers de comptes, où chaque transaction implique des calculs similaires. Les légères inexactitudes s'accumulent, et avant que vous ne le réalisiez, vos états financiers sont décalés de milliers d’euros. Et honnêtement, on est tous tous d'accord sur le fait que lorsqu’il s'agit d'argent, aucune erreur n’est tolérable : à la fois légalement mais aussi pour établir une relation de confiance avec vos clients.

Pourquoi une telle erreur ?

La première question que je me suis posée en rencontrant le problème dans l'un de mes projets est d’où vient ce phénomène ? J'ai découvert que la source du problème n'est pas JavaScript, et ces imprécisions affectent également d'autres langages de programmation modernes (Java, C, Python, …).

En fait, la cause réside dans la norme utilisée par ces langages pour représenter les nombres à virgule flottante : le format à virgule flottante en double (ou simple) précision, spécifié par la norme IEEE 754.

La Norme IEEE 754 : une histoire de représentation et de bits

En JavaScript, le type natif number est représenté par des nombres flottants en double précision, ce qui signifie qu'un nombre est encodé avec 64 bits et divisé en trois parties :

  • 1 bit pour le signe
  • 11 bits pour l'exposant
  • 52 bits pour la mantisse (ou fraction) qui varie de 0 à 1

(source : https://en.m.wikipedia.org/wiki/File:IEEE_754_Double_Floating_Point_Format.svg)

Ensuite, vous devez utiliser la formule suivante pour convertir votre représentation en bits en une valeur décimale :

Un exemple de représentation de nombre flottant en double précision, 1/3 :

Ce format permet de représenter une vaste gamme de valeurs, mais il ne peut pas représenter chaque nombre possible avec une précision absolue (il suffit de chercher entre 0 et 1, vous pouvez trouver une infinité de nombres…). Beaucoup de nombres ne peuvent pas être représentés exactement en forme binaire. Pour revenir au premier exemple, c’est le problème avec 0.1 et 0.2. La représentation en double précision nous donne une approximation de ces valeurs, donc lorsque vous ajoutez ces deux représentations imprécises, le résultat n'est pas non plus exact.

Une solution possible : l'arithmétique décimale arbitraire

Maintenant que vous êtes entièrement convaincu que gérer les montants monétaires avec le type number natif de JavaScript est une mauvaise idée (du moins j'espère que vous commencez à en douter), la question à 1 milliard d’euros est comment devez-vous procéder ? Une solution pourrait être d'utiliser un des puissants packages d'arithmétique à précision fixe disponibles en JavaScript. Par exemple Decimal.js (qui est utilisé par l’ORM Prisma pour représenter son type Decimal) ou Big.js.

Ces packages vous fournissent des types de données spéciaux qui vous permettent d'effectuer des calculs sans les erreurs de précision que nous avons expliquées ci-dessus

Cette approche vous offre un autre avantage, elle étend considérablement la valeur numérique maximale pouvant être représentée, ce qui peut s’avérer très utile lorsque vous traitez avec de très gros montants ou des crypto-monnaies.

Même si cette approche peut être vraiment robuste, ce n’est pas celle que j’ai choisie dans mes différents projets d’application web. Je trouve plus simple et plus clair d'appliquer la stratégie du géant des service de payment Stripe et de ne traiter qu'avec des valeurs entières.

Apprendre des maîtres : Stripe, une stratégie sans nombre flottant

Chez Theodo Fintech, nous apprécions le pragmatisme ! Nous aimons nous inspirer des entreprises les plus performantes du secteur. Stripe, la célèbre entreprise de services de paiement, a choisi de gérer les montants d'argent sans nombres flottants mais avec des entiers. Pour ce faire, ils utilisent la plus petite unité de la devise pour représenter un montant monétaire. 

Les unités minimales de monnaie… ne sont pas consistantes !

Je suppose que beaucoup d'entre vous le savent déjà : toutes les monnaies n'ont pas des unités minimales du même ordre de grandeur. La plupart d'entre elles sont des monnaies dites à « deux décimales » (EUR, USD, GBP), ce qui signifie que leur unité minimale est 1/100ème de la devise. Cependant, il existe également des monnaies à « trois décimales » (KWD) ou même des monnaies à « zéro décimale » (JPY). (Vous pouvez trouver plus d'information à ce sujet en consultant la norme ISO4217). Pour gérer ces disparités, vous devez intégrer à votre représentation de données monétaires le facteur multiplicatif permettant de convertir un montant représenté dans la plus petite unité dans la devise correspondante.

J'ai choisi une méthode pour représenter les montants d'argent… OK, mais maintenant, comment les arrondir ?

Je suppose que vous l'avez déjà compris, que vous travailliez avec des nombres natifs, des packages d'arithmétique à précision arbitraire ou des entiers, les calculs peuvent (et vont) vous mener à des résultats flottants que vous devrez arrondir à votre précision monétaire finie. Comme un petit exemple n'est jamais de trop, supposons que vous travaillez avec des valeurs entières et que vous avez contracté un prêt de 16k$ avec un taux d'intérêt très précis de 8,5413% (aïe…). Vous devez alors rembourser 16k$ plus un montant supplémentaire de :

Le cœur du problème est de bien gérer le processus d'arrondissement des montants monétaires après des phases de calcul. La plupart du temps, vous devrez choisir entre trois types d'arrondissement différents :

  • Arrondissement classique : Arrondir à la valeur la plus proche et, si vous êtes à mi-chemin entre deux valeurs, arrondir vers le haut
  • Arrondissement bancaire : Arrondir à la valeur la plus proche et, si vous êtes à mi-chemin entre deux valeurs, arrondir vers le bas si le nombre est pair, et arrondir vers le haut si le nombre est impair (cela vous donne une stabilité numérique lorsque vous devez effectuer de nombreux arrondissements)
  • Arrondissement personnalisé : basé sur la législation en vigueur pour la devise que vous utilisez et le cas d'utilisation que vous traitez

Quand il s'agit d'arrondissements, il n'y a pas vraiment de « recette miracle » : vous devez arbitrer en fonction de votre situation. Je vous recommande de toujours vérifier la législation lorsque vous traitez avec une nouvelle devise et dans un nouveau cas d’usage (conversion, partage d'argent, taux d'intérêt pour les crédits, …). Il vaut mieux suivre les réglementations dès le début pour éviter des problèmes ultérieurs. Par exemple, en ce qui concerne les taux de conversion, la plupart des devises ont des règles déterminées concernant la précision requise et les règles d'arrondissement (vous pouvez consulter les règles de taux de conversion EUR ici).

Conclusion

Cet article n'est pas exhaustif quant à toutes les possibilités existantes pour gérer les montants monétaires en JavaScript et n'a pas non plus l'intention de vous fournir un modèle de données monétaires complet/parfait. J'ai essayé de vous donner suffisamment de billes et de lignes directrices pour mettre en œuvre une représentation éprouvée qui s'est avérée cohérente, résiliente et choisie par de grands acteurs de l'industrie de la fintech. J'espère que vous serez capable d'effectuer des calculs de montants monétaires dans vos futurs projets sans oublier aucun centime dans votre poche en cours de route !