Präzise Finanzberechnung in JavaScript. Was sind die Fallstricke?

125

Um plattformübergreifenden Code zu erstellen, möchte ich eine einfache Finanzanwendung in JavaScript entwickeln. Die erforderlichen Berechnungen umfassen Zinseszinsen und relativ lange Dezimalzahlen. Ich würde gerne wissen, welche Fehler bei der Verwendung von JavaScript für diese Art von Mathematik zu vermeiden sind - wenn dies überhaupt möglich ist!

james_womack
quelle

Antworten:

107

Sie sollten Ihre Dezimalwerte wahrscheinlich um 100 skalieren und alle Geldwerte in ganzen Cent darstellen. Dies dient dazu, Probleme mit der Gleitkomma-Logik und der Arithmetik zu vermeiden . In JavaScript gibt es keinen dezimalen Datentyp - der einzige numerische Datentyp ist Gleitkomma. Daher wird allgemein empfohlen, Geld als 2550Cent anstelle von 25.50Dollar zu behandeln.

Beachten Sie Folgendes in JavaScript:

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

Aber:

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

Der Ausdruck 0.1 + 0.2 === 0.3kehrt zurück false, aber glücklicherweise ist die Ganzzahlarithmetik im Gleitkomma genau, sodass Dezimaldarstellungsfehler durch Skalieren von 1 vermieden werden können .

Beachten Sie, dass während die Menge der reellen Zahlen unendlich ist, nur eine endliche Anzahl von ihnen (18.437.736.874.454.810.627, um genau zu sein) genau durch das JavaScript-Gleitkommaformat dargestellt werden kann. Daher ist die Darstellung der anderen Zahlen eine Annäherung an die tatsächliche Zahl 2 .


1 Douglas Crockford: JavaScript: Die guten Teile : Anhang A - Schreckliche Teile (Seite 105) .
2 David Flanagan: JavaScript: The Definitive Guide, 4. Ausgabe : 3.1.3 Gleitkomma-Literale (Seite 31) .

Daniel Vassallo
quelle
3
Und zur Erinnerung: Runden Sie Berechnungen immer auf den Cent und tun Sie dies auf die für den Verbraucher am wenigsten vorteilhafte Weise. IE Wenn Sie Steuern berechnen, runden Sie auf. Wenn Sie die verdienten Zinsen berechnen, kürzen Sie diese.
Josh
6
@Cirrostratus: Möglicherweise möchten Sie stackoverflow.com/questions/744099 überprüfen . Wenn Sie mit der Skalierungsmethode fortfahren, möchten Sie Ihren Wert im Allgemeinen mit der Anzahl der Dezimalstellen skalieren, die Sie beibehalten möchten. Wenn Sie 2 Dezimalstellen benötigen, skalieren Sie um 100, wenn Sie 4 benötigen, skalieren Sie um 10000.
Daniel Vassallo
2
... In Bezug auf den Wert 3000.57, ja, wenn Sie diesen Wert in JavaScript-Variablen speichern und damit rechnen möchten, möchten Sie ihn möglicherweise skaliert auf 300057 (Anzahl der Cent) speichern. Da 3000.57 + 0.11 === 3000.68kehrt false.
Daniel Vassallo
7
Pennies statt Dollar zu zählen hilft nicht. Wenn Sie Pennys zählen, verlieren Sie die Fähigkeit, einer Ganzzahl bei etwa 10 ^ 16 1 hinzuzufügen. Wenn Sie Dollar zählen, verlieren Sie die Fähigkeit, einer Zahl bei 10 ^ 14 0,01 hinzuzufügen. So oder so ist es auch.
Hiebwaffe
1
Ich habe gerade ein npm- und Bower-Modul erstellt , das hoffentlich bei dieser Aufgabe helfen soll!
Pensierinmusica
18

Es ist die Lösung, jeden Wert mit 100 zu skalieren. Es ist wahrscheinlich nutzlos, es von Hand zu machen, da Sie Bibliotheken finden können, die das für Sie tun. Ich empfehle moneysafe, das eine funktionale API bietet, die sich gut für ES6-Anwendungen eignet:

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

https://github.com/ericelliott/moneysafe

Funktioniert sowohl in Node.js als auch im Browser.

François Zaninotto
quelle
2
Upvoted. Der Punkt "Skalieren nach 100" wird bereits in der akzeptierten Antwort behandelt. Es ist jedoch gut, dass Sie eine Softwarepaketoption mit moderner JavaScript-Syntax hinzugefügt haben. FWIW die Wertnamen in$, $ sind für jemanden mehrdeutig, der das Paket zuvor nicht verwendet hat. Ich weiß, es war Erics Entscheidung, die Dinge so zu benennen, aber ich denke immer noch, dass es ein Fehler genug ist, dass ich sie wahrscheinlich in der import / destructured require-Anweisung umbenennen würde.
James_womack
4
Die Skalierung um 100 hilft nur, bis Sie anfangen möchten, Prozentsätze zu berechnen (im Wesentlichen Division durchführen).
Pointy
2
Ich wünschte, ich könnte einen Kommentar mehrmals positiv bewerten. Eine Skalierung um 100 reicht einfach nicht aus. Der einzige numerische Datentyp in JavaScript ist immer noch ein Gleitkomma-Datentyp, und es werden immer noch erhebliche Rundungsfehler auftreten.
Craig
1
Und noch ein Kopf aus der Readme : Money$afe has not yet been tested in production at scale.. Ich möchte nur darauf hinweisen, damit jeder überlegen kann, ob dies für seinen Anwendungsfall geeignet ist
Nobita,
7

Es gibt keine "präzise" Finanzberechnung wegen nur zwei Dezimalbruchstellen, aber das ist ein allgemeineres Problem.

In JavaScript können Sie jeden Wert um 100 skalieren und jedes Mal verwenden, Math.round()wenn ein Bruch auftreten kann.

Sie können ein Objekt verwenden, um die Zahlen zu speichern und die Rundung in die Prototypenmethode aufzunehmen valueOf(). So was:

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

Auf diese Weise wird jedes Mal, wenn Sie ein Money-Objekt verwenden, es auf zwei Dezimalstellen gerundet dargestellt. Der ungerundete Wert ist weiterhin über erreichbar m.amount.

Money.prototype.valueOf()Wenn Sie möchten , können Sie Ihren eigenen Rundungsalgorithmus einbauen .

selfawaresoup
quelle
Ich mag diesen objektorientierten Ansatz, die Tatsache, dass das Money-Objekt beide Werte enthält, ist sehr nützlich. Dies ist genau die Art von Funktionalität, die ich in meinen benutzerdefinierten Objective-C-Klassen erstellen möchte.
James_womack
4
Es ist nicht genau genug, um zu runden.
Henry Tseng
3
Sollte nicht sys.puts (m + n); //80.76 liest tatsächlich sys.puts (m + n); //80.77? Ich glaube, Sie haben vergessen, die .5 aufzurunden.
Dave L
2
Diese Art von Ansatz weist eine Reihe subtiler Probleme auf, die auftreten können. Zum Beispiel haben Sie keine sicheren Methoden zum Addieren, Subtrahieren, Multiplizieren usw. implementiert, sodass beim Kombinieren von Geldbeträgen wahrscheinlich Rundungsfehler auftreten
1800 INFORMATION
2
Das Problem hierbei ist, dass z. B. Money(0.1)bedeutet, dass der JavaScript-Lexer die Zeichenfolge "0.1" aus der Quelle liest und sie dann in einen binären Gleitkomma konvertiert und Sie dann bereits eine unbeabsichtigte Rundung durchgeführt haben. Das Problem ist die Darstellung (binär oder dezimal), nicht die Genauigkeit .
mgd
3

benutze decimaljs ... Es ist eine sehr gute Bibliothek, die einen harten Teil des Problems löst ...

Verwenden Sie es einfach in Ihrem gesamten Betrieb.

https://github.com/MikeMcl/decimal.js/

tlejmi
quelle
2

Ihr Problem beruht auf Ungenauigkeiten bei Gleitkommaberechnungen. Wenn Sie nur das Runden verwenden, um dies zu lösen, treten beim Multiplizieren und Dividieren größere Fehler auf.

Die Lösung ist unten, eine Erklärung folgt:

Sie müssen über die Mathematik dahinter nachdenken, um sie zu verstehen. Reelle Zahlen wie 1/3 können in der Mathematik nicht mit Dezimalwerten dargestellt werden, da sie endlos sind (z. B. - .333333333333333 ...). Einige Dezimalzahlen können nicht korrekt binär dargestellt werden. Beispielsweise kann 0.1 mit einer begrenzten Anzahl von Ziffern nicht korrekt binär dargestellt werden.

Eine ausführlichere Beschreibung finden Sie hier: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Schauen Sie sich die Lösungsimplementierung an: http://floating-point-gui.de/languages/javascript/

Henry Tseng
quelle
1

Aufgrund der binären Natur ihrer Codierung können einige Dezimalzahlen nicht mit perfekter Genauigkeit dargestellt werden. Beispielsweise

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

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

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

Wenn Sie reines Javascript verwenden müssen, müssen Sie für jede Berechnung über eine Lösung nachdenken. Für den obigen Code können wir Dezimalstellen in ganze Ganzzahlen konvertieren.

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

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

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

Vermeiden von Problemen mit Dezimalmathematik in JavaScript

Es gibt eine spezielle Bibliothek für Finanzberechnungen mit hervorragender Dokumentation. Finance.js

Ali Rehman
quelle
Ich mag, dass Finance.js auch Beispiel-Apps hat
james_womack
0

Leider ignorieren alle bisherigen Antworten die Tatsache, dass nicht alle Währungen 100 Untereinheiten haben (z. B. ist der Cent die Untereinheit des US-Dollars (USD)). Währungen wie der Irakische Dinar (IQD) haben 1000 Untereinheiten: Ein Irakischer Dinar hat 1000 Dateien. Der japanische Yen (JPY) hat keine Untereinheiten. "Mit 100 multiplizieren, um eine ganzzahlige Arithmetik durchzuführen" ist also nicht immer die richtige Antwort.

Zusätzlich müssen Sie für Geldberechnungen die Währung im Auge behalten. Sie können einer indischen Rupie (INR) keinen US-Dollar (USD) hinzufügen (ohne zuvor einen in den anderen umzurechnen).

Es gibt auch Einschränkungen hinsichtlich der maximalen Menge, die durch den ganzzahligen Datentyp von JavaScript dargestellt werden kann.

Bei Geldberechnungen müssen Sie auch berücksichtigen, dass Geld eine endliche Genauigkeit hat (normalerweise 0-3 Dezimalstellen) und die Rundung auf bestimmte Weise erfolgen muss (z. B. "normale" Rundung im Vergleich zur Bankrundung). Die Art der durchzuführenden Rundung kann auch je nach Gerichtsbarkeit / Währung variieren.

Der Umgang mit Geld in Javascript hat eine sehr gute Diskussion der relevanten Punkte.

Bei meinen Suchen habe ich die Bibliothek dinero.js gefunden , die viele Probleme bei Geldberechnungen behandelt . Ich habe es noch nicht in einem Produktionssystem verwendet und kann daher keine fundierte Meinung dazu abgeben.

markvgti
quelle