Calcolo finanziario preciso in JavaScript. Quali sono i trucchi?


125

Nell'interesse della creazione di codice multipiattaforma, vorrei sviluppare una semplice applicazione finanziaria in JavaScript. I calcoli richiesti riguardano l'interesse composto e numeri decimali relativamente lunghi. Mi piacerebbe sapere quali errori evitare quando si utilizza JavaScript per fare questo tipo di matematica, ammesso che sia possibile!

Risposte:


107

Probabilmente dovresti ridimensionare i tuoi valori decimali di 100 e rappresentare tutti i valori monetari in centesimi interi. Questo per evitare problemi con la logica in virgola mobile e l'aritmetica . Non esiste un tipo di dati decimale in JavaScript: l'unico tipo di dati numerico è a virgola mobile. Pertanto, in genere si consiglia di gestire il denaro come 2550centesimi anziché come 25.50dollari.

Considera che in JavaScript:

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

Ma:

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

L'espressione 0.1 + 0.2 === 0.3restituisce false, ma fortunatamente l'aritmetica intera in virgola mobile è esatta, quindi è possibile evitare errori di rappresentazione decimale ridimensionando 1 .

Si noti che mentre l'insieme di numeri reali è infinito, solo un numero finito di essi (18.437.736.874.454.810.627 per l'esattezza) può essere rappresentato esattamente dal formato a virgola mobile JavaScript. Pertanto la rappresentazione degli altri numeri sarà un'approssimazione del numero effettivo 2 .


1 Douglas Crockford: JavaScript: The Good Parts : Appendice A - Awful Parts (pagina 105) .
2 David Flanagan: JavaScript: The Definitive Guide, quarta edizione : 3.1.3 Valori letterali in virgola mobile (pagina 31) .


3
E come promemoria, arrotondate sempre i calcoli al centesimo e fatelo nel modo meno vantaggioso per il consumatore, ovvero se state calcolando le tasse, arrotondate per eccesso. Se stai calcolando l'interesse guadagnato, troncare.
Josh

6
@Cirrostratus: potresti voler controllare stackoverflow.com/questions/744099 . Se si procede con il metodo di ridimensionamento, in generale si vorrebbe scalare il valore in base al numero di cifre decimali che si desidera mantenere la precisione. Se hai bisogno di 2 decimali scala di 100, se ti serve 4 scala di 10000.
Daniel Vassallo

2
... Per quanto riguarda il valore 3000,57, sì, se si memorizza quel valore in variabili JavaScript e si intende eseguire operazioni aritmetiche su di esso, si potrebbe desiderare di memorizzarlo ridimensionato a 300057 (numero di centesimi). Perché 3000.57 + 0.11 === 3000.68ritorna false.
Daniele Vassallo

7
Contare i penny invece dei dollari non aiuterà. Quando si contano i penny, si perde la possibilità di aggiungere 1 a un numero intero a circa 10 ^ 16. Quando si contano i dollari si perde la possibilità di aggiungere 0,01 a un numero a 10 ^ 14. È lo stesso in ogni caso.
Slashingweapon

1
Ho appena creato un modulo npm e Bower che, si spera, dovrebbero aiutare con questo compito!
Pensierinmusica

18

Scalare ogni valore di 100 è la soluzione. Farlo a mano è probabilmente inutile, dal momento che puoi trovare librerie che lo fanno per te. Consiglio moneysafe, che offre un'API funzionale adatta per le applicazioni ES6:

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

https://github.com/ericelliott/moneysafe

Funziona sia in Node.js che nel browser.


2
Upvoted. Il punto "scala di 100" è già trattato nella risposta accettata, tuttavia è positivo che tu abbia aggiunto un'opzione del pacchetto software con la moderna sintassi JavaScript. FWIW i in$, $ nomi dei valori sono ambigui per qualcuno che non ha utilizzato il pacchetto prima. So che è stata una scelta di Eric nominare le cose in questo modo, ma ritengo ancora che sia un errore sufficiente che probabilmente le rinominerei nell'istruzione require import / destructured.
james_womack

4
Il ridimensionamento di 100 aiuta solo fino a quando non inizi a voler fare qualcosa come calcolare le percentuali (eseguire la divisione, essenzialmente).
Pointy

2
Vorrei poter votare un commento più volte. Il ridimensionamento di 100 non è sufficiente. L'unico tipo di dati numerico in JavaScript è ancora un tipo di dati a virgola mobile e ti ritroverai comunque con errori di arrotondamento significativi.
Craig

1
E un altro testa a testa dal readme: Money$afe has not yet been tested in production at scale.. Solo sottolineando questo in modo che chiunque possa quindi considerare se è appropriato per il proprio caso d'uso
Nobita

7

Non esiste un calcolo finanziario "preciso" a causa di due sole cifre decimali, ma questo è un problema più generale.

In JavaScript, puoi scalare ogni valore di 100 e utilizzare Math.round() ogni volta che può verificarsi una frazione.

È possibile utilizzare un oggetto per memorizzare i numeri e includere l'arrotondamento nel valueOf()metodo dei prototipi . Come questo:

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

In questo modo, ogni volta che utilizzi un oggetto Money, verrà rappresentato arrotondato a due decimali. Il valore non arrotondato è ancora accessibile tramite m.amount.

Puoi creare il tuo algoritmo di arrotondamento in Money.prototype.valueOf(), se lo desideri.


Mi piace questo approccio orientato agli oggetti, il fatto che l'oggetto Money contenga entrambi i valori è molto utile. È il tipo esatto di funzionalità che mi piace creare nelle mie classi Objective-C personalizzate.
james_womack

4
Non è abbastanza preciso da arrotondare.
Henry Tseng

3
Non dovrebbe sys.puts (m + n); //80.76 legge effettivamente sys.puts (m + n); //80.77? Credo che tu abbia dimenticato di arrotondare il .5 per eccesso.
Dave L

2
Questo tipo di approccio ha una serie di problemi sottili che possono sorgere. Ad esempio, non hai implementato metodi sicuri di addizione, sottrazione, moltiplicazione e così via, quindi è probabile che si verifichino errori di arrotondamento quando si combinano gli importi in denaro
1800 INFORMAZIONI

2
Il problema qui è che, ad esempio, Money(0.1)significa che il lexer JavaScript legge la stringa "0.1" dal sorgente e poi la converte in una virgola mobile binaria e quindi hai già eseguito un arrotondamento non intenzionale. Il problema riguarda la rappresentazione (binaria vs decimale) non la precisione .
mgd


2

Il tuo problema deriva da un'imprecisione nei calcoli in virgola mobile. Se stai usando solo l'arrotondamento per risolvere questo problema, avrai un errore maggiore quando moltiplichi e dividi.

La soluzione è di seguito, segue una spiegazione:

Dovrai pensare alla matematica dietro a questo per capirlo. I numeri reali come 1/3 non possono essere rappresentati in matematica con valori decimali poiché sono infiniti (ad esempio - .333333333333333 ...). Alcuni numeri in decimale non possono essere rappresentati correttamente in binario. Ad esempio, 0.1 non può essere rappresentato in modo binario correttamente con un numero limitato di cifre.

Per una descrizione più dettagliata, guarda qui: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Dai un'occhiata all'implementazione della soluzione: http://floating-point-gui.de/languages/javascript/


1

A causa della natura binaria della loro codifica, alcuni numeri decimali non possono essere rappresentati con una precisione perfetta. Per esempio

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

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

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

Se hai bisogno di usare javascript puro, devi pensare alla soluzione per ogni calcolo. Per il codice sopra possiamo convertire i decimali in interi interi.

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

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

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

Evitare problemi con la matematica decimale in JavaScript

C'è una libreria dedicata per i calcoli finanziari con un'ottima documentazione. Finance.js


Mi piace che Finance.js abbia anche app di esempio
james_womack il

0

Sfortunatamente tutte le risposte finora ignorano il fatto che non tutte le valute hanno 100 sottounità (ad esempio, il cent è la sottounità del dollaro USA (USD)). Valute come il dinaro iracheno (IQD) hanno 1000 sottounità: un dinaro iracheno ha 1000 fils. Lo yen giapponese (JPY) non ha sottounità. Quindi "moltiplicare per 100 per eseguire operazioni aritmetiche su interi" non è sempre la risposta corretta.

Inoltre, per i calcoli monetari è necessario tenere traccia della valuta. Non puoi aggiungere un dollaro statunitense (USD) a una rupia indiana (INR) (senza prima convertirne l'uno nell'altro).

Esistono anche limitazioni sulla quantità massima che può essere rappresentata dal tipo di dati intero di JavaScript.

Nei calcoli monetari devi anche tenere a mente che il denaro ha una precisione finita (tipicamente 0-3 punti decimali) e l'arrotondamento deve essere eseguito in modi particolari (ad esempio, arrotondamento "normale" contro arrotondamento del banchiere). Il tipo di arrotondamento da eseguire potrebbe anche variare in base alla giurisdizione / valuta.

Come gestire il denaro in javascript ha un'ottima discussione sui punti rilevanti.

Nelle mie ricerche ho trovato la libreria dinero.js che risolve molti dei problemi rispetto ai calcoli monetari . Non l'ho ancora utilizzato in un sistema di produzione, quindi non posso dare un'opinione informata al riguardo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.