Calcul financier précis en JavaScript. Quels sont les Gotchas?


125

Dans l'intérêt de créer du code multiplateforme, j'aimerais développer une application financière simple en JavaScript. Les calculs nécessaires impliquent des intérêts composés et des nombres décimaux relativement longs. J'aimerais savoir quelles erreurs éviter lors de l'utilisation de JavaScript pour faire ce type de calcul - si c'est possible!

Réponses:


107

Vous devriez probablement mettre à l'échelle vos valeurs décimales par 100 et représenter toutes les valeurs monétaires en cents entiers. Ceci permet d'éviter les problèmes de logique et d'arithmétique à virgule flottante . Il n'y a pas de type de données décimal en JavaScript - le seul type de données numérique est à virgule flottante. Par conséquent, il est généralement recommandé de traiter l'argent sous forme de 2550centimes au lieu de 25.50dollars.

Considérez cela en JavaScript:

var result = 1.0 + 2.0;     // (result === 3.0) returns true

Mais:

var result = 0.1 + 0.2;     // (result === 0.3) returns false

L'expression 0.1 + 0.2 === 0.3retourne false, mais heureusement l'arithmétique entière en virgule flottante est exacte, donc les erreurs de représentation décimale peuvent être évitées en mettant à l'échelle 1 .

Notez que si l'ensemble des nombres réels est infini, seul un nombre fini d'entre eux (18 437 736 874 454 810 627 pour être exact) peut être représenté exactement par le format à virgule flottante JavaScript. Par conséquent, la représentation des autres nombres sera une approximation du nombre réel 2 .


1 Douglas Crockford: JavaScript: Les bonnes parties : Annexe A - Les horribles parties (page 105) .
2 David Flanagan: JavaScript: le guide définitif, quatrième édition : 3.1.3 littéraux à virgule flottante (page 31) .


3
Et pour rappel, arrondissez toujours les calculs au centime, et faites-le de la manière la moins avantageuse pour le consommateur, c'est-à-dire si vous calculez la taxe, arrondissez. Si vous calculez les intérêts gagnés, tronquez.
Josh

6
@Cirrostratus: vous pouvez consulter stackoverflow.com/questions/744099 . Si vous continuez avec la méthode de mise à l'échelle, en général, vous voudrez mettre à l'échelle votre valeur en fonction du nombre de chiffres décimaux dont vous souhaitez conserver la précision. Si vous avez besoin de 2 décimales, mettez à l'échelle par 100, si vous en avez besoin de 4, par 10 000.
Daniel Vassallo

2
... Concernant la valeur 3000,57, oui, si vous stockez cette valeur dans des variables JavaScript et que vous avez l'intention de faire de l'arithmétique dessus, vous voudrez peut-être la stocker à l'échelle de 300057 (nombre de centimes). Parce que 3000.57 + 0.11 === 3000.68revient false.
Daniel Vassallo

7
Compter quelques centimes au lieu de dollars n'aidera pas. Lorsque vous comptez quelques centimes, vous perdez la possibilité d'ajouter 1 à un entier à environ 10 ^ 16. Lorsque vous comptez des dollars, vous perdez la possibilité d'ajouter 0,01 à un nombre à 10 ^ 14. C'est pareil de toute façon.
slashingweapon

1
Je viens de créer un module npm et Bower qui devrait, espérons-le, vous aider dans cette tâche!
Pensierinmusica

18

Mettre à l'échelle chaque valeur par 100 est la solution. Le faire à la main est probablement inutile, car vous pouvez trouver des bibliothèques qui le font pour vous. Je recommande moneysafe, qui propose une API fonctionnelle bien adaptée aux applications ES6:

const { in$, $ } = require('moneysafe');
console.log(in$($(10.5) + $(.3)); // 10.8

https://github.com/ericelliott/moneysafe

Fonctionne à la fois dans Node.js et dans le navigateur.


2
Vote positif. Le point «échelle par 100» est déjà couvert dans la réponse acceptée, mais il est bon que vous ayez ajouté une option de progiciel avec une syntaxe JavaScript moderne. FWIW, les in$, $ noms de valeur sont ambigus pour quelqu'un qui n'a jamais utilisé le package auparavant. Je sais que c'était le choix d'Eric de nommer les choses de cette façon, mais je pense toujours que c'est une erreur suffisante pour que je les renomme probablement dans l'instruction require import / destructured.
james_womack

4
La mise à l'échelle par 100 n'aide que jusqu'à ce que vous commenciez à vouloir faire quelque chose comme calculer des pourcentages (effectuer une division, essentiellement).
Pointy

2
J'aimerais pouvoir voter plusieurs fois pour un commentaire. La mise à l'échelle par 100 n'est tout simplement pas suffisante. Le seul type de données numérique en JavaScript est toujours un type de données à virgule flottante, et vous allez toujours vous retrouver avec des erreurs d'arrondi importantes.
Craig

1
Et une autre tête vers le haut de la readme: Money$afe has not yet been tested in production at scale.. Il suffit de le signaler pour que tout le monde puisse ensuite déterminer si cela convient à son cas d'utilisation
Nobita

7

Il n'y a pas de calcul financier "précis" à cause de seulement deux chiffres de fraction décimale, mais c'est un problème plus général.

En JavaScript, vous pouvez mettre à l'échelle chaque valeur par 100 et l'utiliser à Math.round()chaque fois qu'une fraction peut se produire.

Vous pouvez utiliser un objet pour stocker les nombres et inclure l'arrondi dans sa valueOf()méthode de prototypes . Comme ça:

sys = require('sys');

var Money = function(amount) {
        this.amount = amount;
    }
Money.prototype.valueOf = function() {
    return Math.round(this.amount*100)/100;
}

var m = new Money(50.42355446);
var n = new Money(30.342141);

sys.puts(m.amount + n.amount); //80.76569546
sys.puts(m+n); //80.76

De cette façon, chaque fois que vous utilisez un objet Money, il sera représenté comme arrondi à deux décimales. La valeur non arrondie est toujours accessible via m.amount.

Vous pouvez intégrer votre propre algorithme d'arrondi dans Money.prototype.valueOf(), si vous le souhaitez.


J'aime cette approche orientée objet, le fait que l'objet Money contienne les deux valeurs est très utile. C'est le type exact de fonctionnalité que j'aime créer dans mes classes Objective-C personnalisées.
james_womack

4
Ce n'est pas assez précis pour arrondir.
Henry Tseng

3
Ne devrait pas sys.puts (m + n); //80.76 lit réellement sys.puts (m + n); //80,77? Je crois que vous avez oublié d'arrondir le .5.
Dave L

2
Ce type d'approche comporte un certain nombre de problèmes subtils qui peuvent surgir. Par exemple, vous n'avez pas mis en œuvre de méthodes sûres d'addition, de soustraction, de multiplication, etc., vous risquez donc de rencontrer des erreurs d'arrondi lors de la combinaison de montants d'argent
1800 INFORMATION

2
Le problème ici est que, par exemple Money(0.1), cela signifie que le lexer JavaScript lit la chaîne "0,1" à partir de la source, puis la convertit en virgule flottante binaire et que vous avez déjà fait un arrondi involontaire. Le problème concerne la représentation (binaire vs décimal) et non la précision .
mgd


2

Votre problème provient de l'inexactitude des calculs en virgule flottante. Si vous utilisez simplement l'arrondi pour résoudre ce problème, vous aurez une plus grande erreur lorsque vous multipliez et divisez.

La solution est ci-dessous, une explication suit:

Vous devrez penser aux mathématiques derrière cela pour le comprendre. Les nombres réels comme 1/3 ne peuvent pas être représentés en maths avec des valeurs décimales car ils sont infinis (par exemple - .333333333333333 ...). Certains nombres en décimal ne peuvent pas être représentés correctement en binaire. Par exemple, 0,1 ne peut pas être représenté correctement en binaire avec un nombre limité de chiffres.

Pour une description plus détaillée, regardez ici: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Jetez un œil à l'implémentation de la solution: http://floating-point-gui.de/languages/javascript/


1

En raison de la nature binaire de leur codage, certains nombres décimaux ne peuvent pas être représentés avec une précision parfaite. Par exemple

var money = 600.90;
var price = 200.30;
var total = price * 3;

// Outputs: false
console.log(money >= total);

// Outputs: 600.9000000000001
console.log(total);

Si vous avez besoin d'utiliser du javascript pur, vous devez penser à une solution pour chaque calcul. Pour le code ci-dessus, nous pouvons convertir des décimales en entiers entiers.

var money = 60090;
var price = 20030;
var total = price * 3;

// Outputs: true
console.log(money >= total);

// Outputs: 60090
console.log(total);

Éviter les problèmes avec les mathématiques décimales dans JavaScript

Il existe une bibliothèque dédiée aux calculs financiers avec une excellente documentation. Finance.js


J'aime que Finance.js propose également des exemples d'applications
james_womack

0

Malheureusement, toutes les réponses jusqu'à présent ignorent le fait que toutes les devises n'ont pas 100 sous-unités (par exemple, le cent est la sous-unité du dollar américain (USD)). Des devises comme le dinar irakien (IQD) ont 1000 sous-unités: un dinar irakien a 1000 fils. Le yen japonais (JPY) n'a pas de sous-unités. Donc "multiplier par 100 pour faire de l'arithmétique entière" n'est pas toujours la bonne réponse.

De plus, pour les calculs monétaires, vous devez également garder une trace de la devise. Vous ne pouvez pas ajouter un dollar américain (USD) à une roupie indienne (INR) (sans d'abord convertir l'un en l'autre).

Il existe également des limitations sur la quantité maximale qui peut être représentée par le type de données entier de JavaScript.

Dans les calculs monétaires, vous devez également garder à l'esprit que l'argent a une précision finie (généralement de 0 à 3 décimales) et que l'arrondi doit être fait de manière particulière (par exemple, arrondi «normal» ou arrondi de la banque). Le type d'arrondi à effectuer peut également varier selon la juridiction / la devise.

Comment gérer l'argent en javascript a une très bonne discussion sur les points pertinents.

Dans mes recherches, j'ai trouvé la bibliothèque dinero.js qui traite de nombreux problèmes liés aux calculs monétaires . Je ne l'ai pas encore utilisé dans un système de production, je ne peux donc pas donner une opinion éclairée à ce sujet.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.