Precyzyjne obliczenia finansowe w JavaScript. Jakie są pułapki?


125

W celu tworzenia kodu wieloplatformowego chciałbym opracować prostą aplikację finansową w JavaScript. Wymagane obliczenia obejmują odsetki składane i stosunkowo długie liczby dziesiętne. Chciałbym wiedzieć, jakich błędów unikać podczas korzystania z JavaScript do wykonywania tego typu obliczeń - jeśli w ogóle jest to możliwe!

Odpowiedzi:


107

Prawdopodobnie powinieneś przeskalować swoje wartości dziesiętne o 100 i przedstawić wszystkie wartości pieniężne w pełnych centach. Ma to na celu uniknięcie problemów z logiką zmiennoprzecinkową i arytmetyką . W JavaScript nie ma danych dziesiętnych - jedynym typem liczbowym jest zmiennoprzecinkowy. Dlatego ogólnie zaleca się traktowanie pieniędzy w 2550centach zamiast25.50 dolarach.

Weź pod uwagę, że w JavaScript:

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

Ale:

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

Wyrażenie 0.1 + 0.2 === 0.3zwraca false, ale na szczęście arytmetyka liczb całkowitych w liczbach zmiennoprzecinkowych jest dokładna, więc można uniknąć błędów w reprezentacji dziesiętnej przez skalowanie 1 .

Zauważ, że chociaż zbiór liczb rzeczywistych jest nieskończony, tylko skończona ich liczba (dokładnie 18 437 736 874 454 810 627) może być dokładnie reprezentowana przez format zmiennoprzecinkowy JavaScript. Dlatego przedstawienie pozostałych liczb będzie przybliżeniem rzeczywistej liczby 2 .


1 Douglas Crockford: JavaScript: Dobre części : Dodatek A - Okropne części (strona 105) .
2 David Flanagan: JavaScript: The Definitive Guide, wydanie czwarte : 3.1.3 Literały zmiennoprzecinkowe (strona 31) .


3
Przypominamy, że zawsze zaokrąglaj obliczenia do centów i rób to w najmniej korzystny dla konsumenta sposób. IE Jeśli obliczasz podatek, zaokrąglij w górę. Jeśli obliczasz zarobione odsetki, obetnij.
Josh

6
@Cirrostratus: Możesz to sprawdzić stackoverflow.com/questions/744099 . Jeśli zastosujesz metodę skalowania, na ogół chciałbyś przeskalować swoją wartość o liczbę cyfr dziesiętnych, dla których chcesz zachować precyzję. Jeśli potrzebujesz 2 miejsc po przecinku, przeskaluj o 100, jeśli potrzebujesz 4, skaluj o 10000.
Daniel Vassallo

2
... Jeśli chodzi o wartość 3000,57, to tak, jeśli przechowujesz tę wartość w zmiennych JavaScript i zamierzasz na niej wykonywać działania arytmetyczne, możesz chcieć zapisać ją przeskalowaną do 300057 (liczba centów). Ponieważ 3000.57 + 0.11 === 3000.68wracafalse .
Daniel Vassallo

7
Liczenie groszy zamiast dolarów nie pomoże. Licząc grosze, tracisz możliwość dodania 1 do liczby całkowitej przy około 10 ^ 16. Podczas liczenia dolarów tracisz możliwość dodania 0,01 do liczby przy 10 ^ 14. Tak czy inaczej jest tak samo.
cięcie

1
Właśnie stworzyłem moduł npm i Bower, który, mam nadzieję, powinien pomóc w tym zadaniu!
Pensierinmusica

18

Rozwiązaniem jest skalowanie każdej wartości o 100. Robienie tego ręcznie jest prawdopodobnie bezużyteczne, ponieważ możesz znaleźć biblioteki, które robią to za Ciebie. Polecam moneysafe, który oferuje funkcjonalne API dobrze dopasowane do aplikacji ES6:

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

https://github.com/ericelliott/moneysafe

Działa zarówno w Node.js, jak iw przeglądarce.


2
Głosowano za. Punkt „skalowanie o 100” jest już uwzględniony w przyjętej odpowiedzi, jednak dobrze, że dodano opcję pakietu oprogramowania z nowoczesną składnią JavaScript. FWIW in$, $ nazwy wartości są niejednoznaczne dla kogoś, kto nie korzystał wcześniej z pakietu. Wiem, że to Eric zdecydował się nazwać rzeczy w ten sposób, ale nadal uważam, że to wystarczający błąd, że prawdopodobnie zmieniłbym ich nazwy w instrukcji import / destructured require.
james_womack

4
Skalowanie o 100 pomaga tylko do momentu, gdy zaczniesz chcieć zrobić coś takiego, jak obliczanie procentów (zasadniczo wykonuj dzielenie).
Pointy

2
Chciałbym móc wielokrotnie głosować za komentarzem. Skalowanie o 100 po prostu nie wystarczy. Jedynym typem danych liczbowych w JavaScript jest nadal typ danych zmiennoprzecinkowych i nadal będziesz mieć poważne błędy zaokrąglania.
Craig

1
A inny heads-up z readme: Money$afe has not yet been tested in production at scale.. Po prostu wskazuję na to, aby każdy mógł rozważyć, czy jest to odpowiednie dla ich przypadku użycia
Nobita

7

Nie ma czegoś takiego jak „precyzyjne” obliczenia finansowe z powodu zaledwie dwóch cyfr po przecinku, ale to jest bardziej ogólny problem.

W JavaScript możesz skalować każdą wartość o 100 i używać jej Math.round()za każdym razem, gdy może wystąpić ułamek.

Możesz użyć obiektu do przechowywania liczb i uwzględnienia zaokrąglenia w jego prototypowej valueOf()metodzie. Lubię to:

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

W ten sposób za każdym razem, gdy użyjesz obiektu Money, będzie on reprezentowany jako zaokrąglony do dwóch miejsc po przecinku. Wartość niezaokrąglona jest nadal dostępna za pośrednictwem m.amount.

Jeśli chcesz, możesz wbudować własny algorytm zaokrąglania w Money.prototype.valueOf().


Podoba mi się to podejście obiektowe, fakt, że obiekt Money przechowuje obie wartości, jest bardzo przydatny. Jest to dokładny typ funkcjonalności, który lubię tworzyć w moich niestandardowych klasach Objective-C.
james_womack

4
Nie jest wystarczająco dokładne, aby zaokrąglić.
Henry Tseng,

3
Nie powinno sys.puts (m + n); //80.76 faktycznie czyta sys.puts (m + n); //80.77? Myślę, że zapomniałeś zaokrąglić .5 w górę.
Dave L

2
Takie podejście wiąże się z szeregiem subtelnych problemów, które mogą się pojawić. Na przykład nie zaimplementowałeś bezpiecznych metod dodawania, odejmowania, mnożenia i tak dalej, więc prawdopodobnie napotkasz błędy zaokrąglania podczas łączenia kwot pieniędzy
1800 INFORMACJE

2
Problem polega na tym, że np. Money(0.1)Oznacza, że ​​lekser JavaScript odczytuje łańcuch „0.1” ze źródła, a następnie konwertuje go na binarną liczbę zmiennoprzecinkową, a następnie wykonałeś już niezamierzone zaokrąglenie. Problem dotyczy reprezentacji (binarna vs dziesiętna), a nie precyzji .
mgd


2

Twój problem wynika z niedokładności w obliczeniach zmiennoprzecinkowych. Jeśli używasz tylko zaokrąglania, aby rozwiązać ten problem, będziesz mieć większy błąd podczas mnożenia i dzielenia.

Rozwiązanie jest poniżej, a wyjaśnienie:

Aby to zrozumieć, musisz pomyśleć o matematyce stojącej za tym. Liczb rzeczywistych, takich jak 1/3, nie można przedstawić w matematyce za pomocą wartości dziesiętnych, ponieważ nie mają one końca (np. - .333333333333333 ...). Niektórych liczb dziesiętnych nie można poprawnie przedstawić binarnie. Na przykład 0,1 nie może być poprawnie reprezentowane binarnie za pomocą ograniczonej liczby cyfr.

Bardziej szczegółowy opis znajdziesz tutaj: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Przyjrzyj się wdrożeniu rozwiązania: http://floating-point-gui.de/languages/javascript/


1

Ze względu na binarny charakter ich kodowania, niektórych liczb dziesiętnych nie można przedstawić z doskonałą dokładnością. Na przykład

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

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

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

Jeśli potrzebujesz używać czystego javascript, musisz pomyśleć o rozwiązaniu dla każdego obliczenia. Dla powyższego kodu możemy zamienić ułamki dziesiętne na całkowite liczby całkowite.

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

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

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

Unikanie problemów z matematyką dziesiętną w JavaScript

Istnieje dedykowana biblioteka do obliczeń finansowych z doskonałą dokumentacją. Finance.js


Podoba mi się, że Finance.js ma też przykładowe aplikacje
james_womack

0

Niestety, wszystkie dotychczasowe odpowiedzi pomijają fakt, że nie wszystkie waluty mają 100 jednostek podrzędnych (np. Cent jest podjednostką dolara amerykańskiego (USD)). Waluty takie jak dinar iracki (IQD) mają 1000 jednostek podrzędnych: dinar iracki ma 1000 fils. Japoński jen (JPY) nie ma podjednostek. Zatem „pomnóż przez 100, aby wykonać arytmetykę liczb całkowitych” nie zawsze jest poprawną odpowiedzią.

Dodatkowo w przypadku obliczeń pieniężnych musisz również śledzić walutę. Nie możesz dodać dolara amerykańskiego (USD) do rupii indyjskiej (INR) (bez wcześniejszej zamiany jednego na drugiego).

Istnieją również ograniczenia dotyczące maksymalnej ilości, którą można przedstawić za pomocą typu danych typu integer JavaScript.

W obliczeniach pieniężnych należy również pamiętać, że pieniądze mają skończoną precyzję (zazwyczaj 0-3 miejsca po przecinku) i należy je zaokrąglać w określony sposób (np. „Normalne” zaokrąglanie w porównaniu do zaokrąglania bankierów). Rodzaj zaokrąglania, które ma być wykonane, może się również różnić w zależności od jurysdykcji / waluty.

Jak radzić sobie z pieniędzmi w javascript ma bardzo dobre omówienie odpowiednich punktów.

Podczas moich poszukiwań znalazłem bibliotekę dinero.js, która rozwiązuje wiele problemów związanych z obliczeniami pieniężnymi . Nie używałem go jeszcze w systemie produkcyjnym, więc nie mogę dać na jego temat opinii.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.