Jak radzić sobie z precyzją liczb zmiennoprzecinkowych w JavaScript?

617

Mam następujący fałszywy skrypt testowy:

function test() {
  var x = 0.1 * 0.2;
  document.write(x);
}
test();

Spowoduje to wydrukowanie wyniku, 0.020000000000000004podczas gdy powinien po prostu wydrukować 0.02(jeśli korzystasz z kalkulatora). O ile rozumiem, wynika to z błędów w precyzji mnożenia zmiennoprzecinkowego.

Czy ktoś ma dobre rozwiązanie, aby w takim przypadku uzyskać prawidłowy wynik 0.02? Wiem, że istnieją funkcje takie jak toFixedlub zaokrąglanie byłoby inną możliwością, ale chciałbym naprawdę wydrukować cały numer bez żadnego cięcia i zaokrąglania. Chciałem tylko wiedzieć, czy któryś z was ma jakieś fajne, eleganckie rozwiązanie.

Oczywiście, w przeciwnym razie zaokrąglę do około 10 cyfr.

Juri
źródło
118
W rzeczywistości błąd polega na tym, że nie ma możliwości zmapowania 0.1na skończoną binarną liczbę zmiennoprzecinkową.
Aaron Digulla,
10
Większość ułamków nie może być konwertowana z dokładnością do dziesiętnej. Dobre wyjaśnienie znajduje się tutaj: docs.python.org/release/2.5.1/tut/node16.html
Nate Zaugg
7
możliwy duplikat Czy matematyka JavaScript jest zepsuta?
epascarello,
53
@ SalmanA: To, że środowisko wykonawcze JavaScript ukrywa przed tobą ten problem, nie oznacza, że ​​się mylę.
Aaron Digulla,
5
Nie zgadzam się z Aaronem, istnieją sposoby na perfekcyjne i kompletne kodowanie kodu binarnego 0.1. Ale IEEE 754 niekoniecznie to definiuje. Wyobraź sobie reprezentację, w której z jednej strony kodowałbyś część całkowitą w postaci binarnej, z drugiej strony część dziesiętną, aż do n miejsc po przecinku, również w postaci binarnej, jak normalna liczba całkowita> 0, a na końcu pozycja punktu dziesiętnego . Cóż, reprezentowałbyś 0,1 idealnie, bez błędów. Przy okazji, ponieważ JS używa wewnętrznie skończonej liczby miejsc po przecinku, deweloperzy równie dobrze mogą kodować wnętrzności, aby nie popełnić tego błędu na ostatnich miejscach po przecinku.
Fabien Haddadi

Odpowiedzi:

469

Z przewodnika zmiennoprzecinkowego :

Co mogę zrobić, aby uniknąć tego problemu?

To zależy od tego, jakie obliczenia wykonujesz.

  • Jeśli naprawdę potrzebujesz dokładnych wyników, zwłaszcza gdy pracujesz z pieniędzmi: użyj specjalnego typu danych dziesiętnych.
  • Jeśli po prostu nie chcesz wyświetlać wszystkich tych dodatkowych miejsc po przecinku: po prostu sformatuj wynik zaokrąglony do stałej liczby miejsc po przecinku podczas jego wyświetlania.
  • Jeśli nie masz dostępnego typu danych dziesiętnych, alternatywą jest praca z liczbami całkowitymi, np. Wykonywanie obliczeń pieniężnych całkowicie w centach. Ale to więcej pracy i ma pewne wady.

Zauważ, że pierwszy punkt ma zastosowanie tylko wtedy, gdy naprawdę potrzebujesz określonego precyzyjnego zachowania po przecinku . Większość ludzi tego nie potrzebuje, są po prostu zirytowani, że ich programy nie działają poprawnie z liczbami takimi jak 1/10, nie zdając sobie sprawy, że nawet nie mrugną z powodu tego samego błędu, jeśli wystąpiłby z 1/3.

Jeśli pierwszy punkt naprawdę dotyczy Ciebie, użyj BigDecimal dla JavaScript , który wcale nie jest elegancki, ale faktycznie rozwiązuje problem, a nie zapewnia niedoskonałego obejścia.

Michael Borgwardt
źródło
11
Zauważyłem twój martwy link do BigDecimal i szukając lustra, znalazłem alternatywę o nazwie BigNumber: jsfromhell.com/classes/bignumber
Jacksonkr
4
@ bass-t: Tak, ale liczby zmiennoprzecinkowe mogą dokładnie reprezentować liczby całkowite do długości znacznika, a zgodnie ze standardem ECMA jest liczbą zmiennoprzecinkową 64-bitową. Więc może dokładnie reprezentować liczby całkowite do 2 ^ 52
Michael Borgwardt
5
@Karl: Ułamek dziesiętny 1/10 nie może być reprezentowany jako skończona ułamek binarny w bazie 2, i takie są liczby Javascript. Jest to w rzeczywistości dokładnie ten sam problem.
Michael Borgwardt,
12
Dowiedziałem się dzisiaj, że nawet liczby całkowite mają problemy z precyzją w javascript. Pomyśl, że console.log(9332654729891549)faktycznie drukuje 9332654729891548(tzn. Wyłącza jeden!)
ml
12
@mlathe: Doh .. ;P... Pomiędzy 2⁵²= 4,503,599,627,370,496i 2⁵³= 9,007,199,254,740,992reprezentowalne liczby są dokładnie liczbami całkowitymi . Na następnym przedziale od 2⁵³do 2⁵⁴, wszystko jest mnożona przez2 , więc zakodowania numery są nawet te , itd. Z drugiej strony, na poprzednim przedziale od 2⁵¹do 2⁵², odstęp jest 0.5, itd. Wynika to po prostu zwiększając | zmniejszenie podstawy | radix 2 | wykładnik binarny w / z 64-bitowej wartości zmiennoprzecinkowej (co z kolei wyjaśnia rzadko udokumentowane „nieoczekiwane” zachowanie toPrecision()wartości pomiędzy 0i 1).
GitaarLAB
126

Lubię rozwiązanie Pedro Ladarii i używam czegoś podobnego.

function strip(number) {
    return (parseFloat(number).toPrecision(12));
}

W przeciwieństwie do rozwiązania Pedros zaokrągli to w górę 0,999 ... powtarzanie i jest dokładne do plus / minus jeden na najmniej znaczącej cyfrze.

Uwaga: W przypadku 32- lub 64-bitowych liczb zmiennoprzecinkowych należy użyć toPrecision (7) i toPrecision (15), aby uzyskać najlepsze wyniki. Zobacz to pytanie, aby dowiedzieć się, dlaczego.

linux_mike
źródło
21
Czy jest jakiś powód, dla którego wybrałeś 12?
qwertymk
18
toPrecisionzwraca ciąg zamiast liczby. Nie zawsze może to być pożądane.
SStanley,
7
parseFloat (1.005) .toPrecision (3) => 1,00
Peter
5
@ user2428118, wiem, chciałem pokazać błąd zaokrąglania, wynik to 1,00 zamiast 1,01
Peter
9
To, co powiedział @ user2428118, może nie być wystarczająco oczywiste: (9.99*5).toPrecision(2)= 50 zamiast 49,95, ponieważ toPrecision liczy liczbę całkowitą, a nie tylko liczby dziesiętne. Możesz wtedy użyć toPrecision(4), ale jeśli wynik wynosi> 100, znów nie masz szczęścia, ponieważ pozwoli to na trzy pierwsze cyfry i jedną dziesiętną, przesuwając kropkę i czyniąc to mniej lub bardziej bezużytecznym. Skończyło się stosując toFixed(2)zamiast
aexl
79

Dla matematycznie skłonnych: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Zalecanym podejściem jest zastosowanie współczynników korekcyjnych (pomnóż przez odpowiednią moc 10, aby arytmetyka zachodzi między liczbami całkowitymi). Na przykład w przypadku 0.1 * 0.2współczynnika korekcji jest 10i wykonujesz obliczenia:

> var x = 0.1
> var y = 0.2
> var cf = 10
> x * y
0.020000000000000004
> (x * cf) * (y * cf) / (cf * cf)
0.02

(Bardzo szybkie) rozwiązanie wygląda mniej więcej tak:

var _cf = (function() {
  function _shift(x) {
    var parts = x.toString().split('.');
    return (parts.length < 2) ? 1 : Math.pow(10, parts[1].length);
  }
  return function() { 
    return Array.prototype.reduce.call(arguments, function (prev, next) { return prev === undefined || next === undefined ? undefined : Math.max(prev, _shift (next)); }, -Infinity);
  };
})();

Math.a = function () {
  var f = _cf.apply(null, arguments); if(f === undefined) return undefined;
  function cb(x, y, i, o) { return x + f * y; }
  return Array.prototype.reduce.call(arguments, cb, 0) / f;
};

Math.s = function (l,r) { var f = _cf(l,r); return (l * f - r * f) / f; };

Math.m = function () {
  var f = _cf.apply(null, arguments);
  function cb(x, y, i, o) { return (x*f) * (y*f) / (f * f); }
  return Array.prototype.reduce.call(arguments, cb, 1);
};

Math.d = function (l,r) { var f = _cf(l,r); return (l * f) / (r * f); };

W tym przypadku:

> Math.m(0.1, 0.2)
0.02

Zdecydowanie polecam użycie przetestowanej biblioteki, takiej jak SinfulJS

SheetJS
źródło
1
Uwielbiam to eleganckie obejście, ale wydaje się, że nie jest idealne: jsfiddle.net/Dm6F5/1 Math.a (76,65, 38,45) zwraca 115,10000000000002
nicolallias
3
Math.m (10 2332226616) podaje mi „-19627406800”, co jest wartością ujemną ... Mam nadzieję, że musi istnieć górna granica - być może to powoduje ten problem. Proszę zasugerować
Shiva Komuravelly,
1
To wszystko wygląda świetnie, ale wydaje się, że gdzieś tam jest błąd.
MrYellow 12.03.15
5
Bardzo szybkie rozwiązanie, które powiedział ... zepsuta poprawka, której nikt nigdy nie powiedział.
Cozzbie,
2
Nie używaj powyższego kodu. To absolutnie nie jest „szybkie rozwiązanie”, jeśli nie działa. To pytanie matematyczne, więc wymagana jest dokładność.
Drenai
49

Czy wykonujesz tylko mnożenie? Jeśli tak, to możesz wykorzystać na swoją korzyść czysty sekret dotyczący arytmetyki dziesiętnej. To jest to NumberOfDecimals(X) + NumberOfDecimals(Y) = ExpectedNumberOfDecimals. To znaczy, że jeśli tak, 0.123 * 0.12to wiemy, że będzie 5 miejsc po przecinku, ponieważ 0.123ma 3 miejsca po przecinku i 0.12ma dwa. Zatem jeśli JavaScript dał nam liczbę taką, 0.014760000002że możemy bezpiecznie zaokrąglić do 5. miejsca po przecinku bez obawy o utratę precyzji.

Nate Zaugg
źródło
6
... i jak uzyskać dokładną liczbę miejsc po przecinku.
line-o
7
0,5 * 0,2 = 0,10; Nadal możesz obcinać z dokładnością do 2 miejsc po przecinku (lub mniej). Ale nigdy nie będzie liczby o znaczeniu matematycznym poza tym prawem.
Nate Zaugg,
3
Czy masz na to powód? Zauważ też, że to samo nie dotyczy podziału.
Griffin,
3
@NateZaugg nie możesz obciąć przepełnionych miejsc po przecinku, musisz zaokrąglić kwotę, ponieważ 2090,5 * 8,61 to 17999.205, ale w liczbach zmiennoprzecinkowych to 17999.204999999998
Lostfields
3
@Lostfields - masz rację! Zaktualizowałem swoją odpowiedź.
Nate Zaugg
29

Szukasz sprintfimplementacji dla JavaScript, abyś mógł wypisywać zmiennoprzecinkowe z niewielkimi błędami (ponieważ są przechowywane w formacie binarnym) w oczekiwanym formacie.

Spróbuj javascript-sprintf , nazwałbyś to tak:

var yourString = sprintf("%.2f", yourNumber);

wydrukować liczbę jako liczbę zmiennoprzecinkową z dwoma miejscami po przecinku.

Możesz także użyć Number.toFixed () do celów wyświetlania, jeśli wolisz nie dołączać więcej plików tylko do zaokrąglania zmiennoprzecinkowego z określoną precyzją.

Douglas
źródło
4
Myślę, że to najczystsze rozwiązanie. Jeśli naprawdę nie potrzebujesz naprawdę 0,02, mały błąd jest znikomy. Wygląda na to, że ważne jest, aby Twój numer był ładnie wyświetlany , a nie że masz dowolną precyzję.
Długi Ouyang,
2
Jest to rzeczywiście najlepsza opcja do wyświetlania, w przypadku skomplikowanych obliczeń sprawdź odpowiedź Borgwardta.
Niedostępne
4
Ale znowu to zwróci dokładnie ten sam ciąg co twojaNumer.toFixed (2).
Robert
27

Uważam, że plik BigNumber.js spełnia moje potrzeby.

Biblioteka JavaScript do arytmetyki dziesiętnej i nie dziesiętnej o dowolnej precyzji.

Ma dobrą dokumentację, a autor bardzo sumiennie odpowiada na opinie.

Ten sam autor ma 2 inne podobne biblioteki:

Big.js

Mała, szybka biblioteka JavaScript do arytmetyki dziesiętnej o dowolnej precyzji. Młodsza siostra na bignumber.js.

i Decimal.js

Typ dziesiętny o dowolnej precyzji dla JavaScript.

Oto kod za pomocą BigNumber:

$(function(){

  
  var product = BigNumber(.1).times(.2);  
  $('#product').text(product);

  var sum = BigNumber(.1).plus(.2);  
  $('#sum').text(sum);


});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>

<!-- 1.4.1 is not the current version, but works for this example. -->
<script src="http://cdn.bootcss.com/bignumber.js/1.4.1/bignumber.min.js"></script>

.1 &times; .2 = <span id="product"></span><br>
.1 &plus; .2 = <span id="sum"></span><br>

Ronnie Overby
źródło
3
Moim zdaniem korzystanie z biblioteki jest zdecydowanie najlepszym wyborem.
Anthony
1
Z tego linku github.com/MikeMcl/big.js/issues/45 bignumber.js -> Financial Decimal.js -> Scientific Big.js -> ???
vee
20
var times = function (a, b) {
    return Math.round((a * b) * 100)/100;
};

---lub---

var fpFix = function (n) {
    return Math.round(n * 100)/100;
};

fpFix(0.1*0.2); // -> 0.02

---również---

var fpArithmetic = function (op, x, y) {
    var n = {
            '*': x * y,
            '-': x - y,
            '+': x + y,
            '/': x / y
        }[op];        

    return Math.round(n * 100)/100;
};

--- jak w ---

fpArithmetic('*', 0.1, 0.2);
// 0.02

fpArithmetic('+', 0.1, 0.2);
// 0.3

fpArithmetic('-', 0.1, 0.2);
// -0.1

fpArithmetic('/', 0.2, 0.1);
// 2
shawndumas
źródło
4
Myślę, że w rezultacie dałoby to ten sam problem. Zwracasz zmiennoprzecinkowy, więc duża szansa, że ​​wartość zwrotu również będzie „niepoprawna”.
Gertjan
1
Bardzo sprytny i użyteczny, +1.
Jonatas Walker
18

Ta funkcja określi potrzebną precyzję na podstawie mnożenia dwóch liczb zmiennoprzecinkowych i zwróci wynik z odpowiednią precyzją. Elegancki nie jest.

function multFloats(a,b){
  var atens = Math.pow(10,String(a).length - String(a).indexOf('.') - 1), 
      btens = Math.pow(10,String(b).length - String(b).indexOf('.') - 1); 
  return (a * atens) * (b * btens) / (atens * btens); 
}
Gabriel
źródło
Ew. Tak, przekonwertujmy liczby na ciągi dla matematyki zmiennoprzecinkowej i zaproponujmy to również jako odpowiedź.
Andrew
16

Co zaskakujące, ta funkcja nie została jeszcze opublikowana, chociaż inni mają podobne odmiany. Pochodzi z dokumentów internetowych MDN dla Math.round (). Jest zwięzły i pozwala na różną precyzję.

function precisionRound(number, precision) {
  var factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}

console.log (precisionRound (1234.5678, 1)); // oczekiwany wynik: 1234,6

console.log (precisionRound (1234.5678, -1)); // oczekiwany wynik: 1230

var inp = document.querySelectorAll('input');
var btn = document.querySelector('button');

btn.onclick = function(){
  inp[2].value = precisionRound( parseFloat(inp[0].value) * parseFloat(inp[1].value) , 5 );
};

//MDN function
function precisionRound(number, precision) {
  var factor = Math.pow(10, precision);
  return Math.round(number * factor) / factor;
}
button{
display: block;
}
<input type='text' value='0.1'>
<input type='text' value='0.2'>
<button>Get Product</button>
<input type='text'>

AKTUALIZACJA: 20 sierpnia 2019 Właśnie zauważyłem ten błąd. Uważam, że jest to spowodowane błędem precyzji zmiennoprzecinkowej w Math.round ().

precisionRound(1.005, 2) // produces 1, incorrect, should be 1.01

Te warunki działają poprawnie:

precisionRound(0.005, 2) // produces 0.01
precisionRound(1.0005, 3) // produces 1.001
precisionRound(1234.5, 0) // produces 1235
precisionRound(1234.5, -1) // produces 1230

Naprawić:

function precisionRoundMod(number, precision) {
  var factor = Math.pow(10, precision);
  var n = precision < 0 ? number : 0.01 / factor + number;
  return Math.round( n * factor) / factor;
}

To dodaje cyfrę po prawej stronie podczas zaokrąglania miejsc po przecinku. MDN zaktualizował stronę Math.round, więc może ktoś mógłby zaoferować lepsze rozwiązanie.

HelloWorldPeace
źródło
zła odpowiedź. 10.2 zawsze zwróci 10.19. jsbin.com/tozogiwide/edit?html,js,console,output
Žilvinas
@ Žilvinas W opublikowanym linku JSBin nie ma funkcji MDN wymienionej powyżej. Myślę, że twój komentarz jest skierowany do niewłaściwej osoby.
HelloWorldPeace,
13

Musisz tylko zdecydować, ile cyfr dziesiętnych rzeczywiście chcesz - nie możesz mieć ciasta i też go zjeść :-)

Błędy numeryczne kumulują się z każdą kolejną operacją, a jeśli nie odetniesz jej wcześnie, po prostu będzie rosła. Biblioteki numeryczne, które prezentują wyniki, które wyglądają na czyste, po prostu odcinają ostatnie 2 cyfry na każdym kroku, koprocesory numeryczne mają również „normalną” i „pełną” długość z tego samego powodu. Cuf-offy są tanie dla procesora, ale bardzo drogie dla ciebie w skrypcie (mnożenie, dzielenie i używanie pov (...)). Dobra biblioteka matematyczna zapewniłaby podłogę (x, n) do wykonania odcięcia dla ciebie.

Przynajmniej powinieneś zrobić globalną zmienną / stałą za pomocą pov (10, n) - co oznacza, że ​​zdecydowałeś się na potrzebną precyzję :-) Następnie wykonaj:

Math.floor(x*PREC_LIM)/PREC_LIM  // floor - you are cutting off, not rounding

Możesz także dalej robić matematykę i tylko odcinać na końcu - zakładając, że wyświetlasz tylko i nie robisz if-ów z wynikami. Jeśli możesz to zrobić, to .toFixed (...) może być bardziej wydajny.

Jeśli robisz porównania if-s / i nie chcesz wycinać, potrzebujesz również małej stałej, zwykle nazywanej eps, która jest o jedno miejsce po przecinku wyższa niż maksymalny oczekiwany błąd. Powiedz, że wartość odcięcia wynosi ostatnie dwa miejsca po przecinku - wtedy twój eps ma 1 na 3 miejscu od ostatniego (3 najmniej znaczący) i możesz go użyć do porównania, czy wynik mieści się w oczekiwanym zakresie eps (0,02 -eps <0,1 * 0,2 <0,02 + eps).

ZXX
źródło
Możesz również dodać 0,5, aby wykonać zaokrąglenie biedaka: Math.floor (x * PREC_LIM + 0,5) / PREC_LIM
cmroanirgo
Uwaga jednak, że na przykład Math.floor(-2.1)jest -3. Więc może użyj np.Math[x<0?'ceil':'floor'](x*PREC_LIM)/PREC_LIM
MikeM
Dlaczego floorzamiast round?
Quinn Comendant
12

Możesz użyć, parseFloat()a toFixed()jeśli chcesz ominąć ten problem w przypadku małej operacji:

a = 0.1;
b = 0.2;

a + b = 0.30000000000000004;

c = parseFloat((a+b).toFixed(2));

c = 0.3;

a = 0.3;
b = 0.2;

a - b = 0.09999999999999998;

c = parseFloat((a-b).toFixed(2));

c = 0.1;
Oprogramowanie
źródło
11

Funkcja round () w phpjs.org działa dobrze: http://phpjs.org/functions/round

num = .01 + .06;  // yields 0.0699999999999
rnum = round(num,12); // yields 0.07
Tomek
źródło
2
@jrg Zgodnie z konwencją liczby kończące się na „5” są zaokrąglane do najbliższej parzystej (ponieważ zawsze zaokrąglanie w górę lub w dół wprowadzałoby błąd w wynikach). Dlatego też 4,725 zaokrąglone do dwóch miejsc po przecinku powinno faktycznie wynosić 4,72.
Mark A. Durham,
9

0.6 * 3 to niesamowite!)) Dla mnie to działa dobrze:

function dec( num )
{
    var p = 100;
    return Math.round( num * p ) / p;
}

Bardzo, bardzo proste))

Олег Всильдеревьев
źródło
Czy to zadziałałoby z czymś takim 8.22e-8 * 1.3?
Paul Carlton
0,6 x 3 = 1,8, kod daje wyniki 2 ... więc nie dobrze.
Zyo
@Zyo W tym przypadku zwraca 1.8. Jak to uruchomiłeś?
Drenai
Ciekawy. W tym miejscu możesz zamienić operatory mnożenia i dzielenia, a to również działa.
Andrew
9

Zauważ, że w przypadku ogólnego zastosowania takie zachowanie może być do zaakceptowania.
Problem pojawia się podczas porównywania wartości zmiennoprzecinkowych w celu ustalenia odpowiedniego działania.
Wraz z pojawieniem się ES6 Number.EPSILONdefiniowana jest nowa stała w celu ustalenia dopuszczalnego marginesu błędu:
Zamiast przeprowadzać takie porównanie

0.1 + 0.2 === 0.3 // which returns false

możesz zdefiniować niestandardową funkcję porównywania, taką jak ta:

function epsEqu(x, y) {
    return Math.abs(x - y) < Number.EPSILON;
}
console.log(epsEqu(0.1+0.2, 0.3)); // true

Źródło: http://2ality.com/2015/04/numbers-math-es6.html#numberepsilon

użytkownik10089632
źródło
W moim przypadku Number.EPSILON był zbyt mały, co spowodowało np.0.9 !== 0.8999999761581421
Toma
8

Otrzymany wynik jest poprawny i dość spójny we wszystkich implementacjach zmiennoprzecinkowych w różnych językach, procesorach i systemach operacyjnych - jedyną rzeczą, która się zmienia, jest poziom niedokładności, gdy liczba zmiennoprzecinkowa jest w rzeczywistości podwójna (lub wyższa).

0,1 w binarnych zmiennoprzecinkowych jest jak 1/3 w systemie dziesiętnym (tj. 0,33333333333333 ... na zawsze), po prostu nie ma dokładnego sposobu na to.

Jeśli masz do czynienia z liczbami zmiennoprzecinkowymi, zawsze oczekuj małych błędów zaokrąglania, więc zawsze będziesz musiał zaokrąglić wyświetlany wynik do czegoś sensownego. W zamian otrzymujesz bardzo szybką i wydajną arytmetykę, ponieważ wszystkie obliczenia są w natywnym pliku binarnym procesora.

W większości przypadków rozwiązaniem nie jest przejście na arytmetykę w punktach stałych, głównie dlatego, że jest znacznie wolniejsza i 99% czasu po prostu nie potrzebujesz dokładności. Jeśli masz do czynienia z rzeczami, które wymagają takiego poziomu dokładności (na przykład transakcje finansowe), JavaScript prawdopodobnie nie jest najlepszym narzędziem do użycia (ponieważ chcesz wymusić typy stałoprzecinkowe, język statyczny jest prawdopodobnie lepszy ).

Szukasz eleganckiego rozwiązania, ale obawiam się, że to jest to: liczby zmiennoprzecinkowe są szybkie, ale mają małe błędy zaokrąglania - zawsze wyświetlają wyniki zaokrąglone do rozsądnych wyników.

Keith
źródło
8

Aby tego uniknąć, powinieneś pracować z wartościami całkowitymi zamiast zmiennoprzecinkowymi. Więc jeśli chcesz mieć 2 pozycje precyzji, pracuj z wartościami * 100, dla 3 pozycji użyj 1000. Podczas wyświetlania użyj formatyzatora, aby wstawić separator.

Wiele systemów pomija pracę z miejscami po przecinku w ten sposób. To jest powód, dla którego wiele systemów pracuje z centami (jako liczbami całkowitymi) zamiast dolarów / euro (jako zmiennoprzecinkowe).

Gertjan
źródło
7

Problem

Zmienny punkt nie może dokładnie przechowywać wszystkich wartości dziesiętnych. Tak więc przy stosowaniu formatów zmiennoprzecinkowych zawsze wystąpią błędy zaokrąglania wartości wejściowych. Błędy na wejściach oczywiście skutkują błędami na wyjściu. W przypadku funkcji dyskretnej lub operatora mogą występować duże różnice na wyjściu wokół punktu, w którym funkcja lub operator są dyskretne.

Dane wejściowe i wyjściowe dla wartości zmiennoprzecinkowych

Tak więc, używając zmiennych zmiennoprzecinkowych, zawsze powinieneś być tego świadomy. I cokolwiek chcesz uzyskać z obliczeń z zmiennoprzecinkowymi, zawsze należy sformatować / uwarunkować przed wyświetleniem z tego względu.
Gdy używane są tylko funkcje ciągłe i operatory, często wystarczą zaokrąglenia do pożądanej precyzji (nie obcinaj). Standardowe funkcje formatowania używane do konwertowania liczb zmiennoprzecinkowych na ciąg zwykle to robią za Ciebie.
Ponieważ zaokrąglanie dodaje błąd, który może spowodować, że całkowity błąd będzie większy niż połowa pożądanej precyzji, dane wyjściowe należy skorygować na podstawie oczekiwanej dokładności danych wejściowych i pożądanej dokładności danych wyjściowych. Powinieneś

  • Zaokrąglij dane wejściowe do oczekiwanej precyzji lub upewnij się, że nie można wprowadzić żadnych wartości z większą precyzją.
  • Dodaj do wyjść małą wartość przed zaokrągleniem / sformatowaniem, która jest mniejsza lub równa 1/4 pożądanej precyzji i większa niż maksymalny oczekiwany błąd spowodowany błędami zaokrąglania na wejściu i podczas obliczania. Jeśli nie jest to możliwe, połączenie precyzji użytego typu danych nie wystarcza, aby zapewnić pożądaną dokładność wyjściową do obliczeń.

Te dwie rzeczy zwykle nie są wykonywane, a w większości przypadków różnice spowodowane ich nie wykonaniem są zbyt małe, aby były ważne dla większości użytkowników, ale miałem już projekt, w którym wyniki nie byłyby akceptowane przez użytkowników bez tych poprawek.

Funkcje dyskretne lub operatory (jak moduł)

Gdy zaangażowane są dyskretne operatory lub funkcje, mogą być wymagane dodatkowe poprawki, aby upewnić się, że wynik jest zgodny z oczekiwaniami. Zaokrąglanie i dodawanie drobnych poprawek przed zaokrąglaniem nie może rozwiązać problemu.
Może być wymagana specjalna kontrola / korekta wyników obliczeń pośrednich, natychmiast po zastosowaniu funkcji dyskretnej lub operatora. W przypadku konkretnego przypadku (operator modułu) zobacz moją odpowiedź na pytanie: Dlaczego operator modułu zwraca liczbę ułamkową w javascript?

Lepiej unikaj problemu

Często bardziej efektywnym jest uniknięcie tych problemów poprzez zastosowanie typów danych (format całkowity lub stały punkt) do obliczeń takich jak ten, które mogą przechowywać oczekiwane dane wejściowe bez błędów zaokrąglania. Przykładem tego jest to, że nigdy nie należy używać wartości zmiennoprzecinkowych do obliczeń finansowych.

Stefan Mondelaers
źródło
4

Spójrz na arytmetykę punktów stałych . Prawdopodobnie rozwiąże Twój problem, jeśli zakres liczb, na których chcesz operować, jest niewielki (np. Waluta). Zaokrąglibym to do kilku wartości dziesiętnych, co jest najprostszym rozwiązaniem.

Marius
źródło
5
Problem nie jest zmiennoprzecinkowy vs. stały, problem jest binarny vs. dziesiętny.
Michael Borgwardt,
4

Wypróbuj moją chiliadyczną bibliotekę arytmetyczną, którą możesz zobaczyć tutaj . Jeśli chcesz późniejszą wersję, mogę ci ją zdobyć.

Robert L.
źródło
4

Nie można dokładnie reprezentować ułamków dziesiętnych dokładnie za pomocą binarnych typów zmiennoprzecinkowych (takiego właśnie używa ECMAScript do reprezentowania wartości zmiennoprzecinkowych). Nie ma więc eleganckiego rozwiązania, chyba że użyjesz dowolnych typów arytmetycznych o precyzji lub typu zmiennoprzecinkowego opartego na liczbach dziesiętnych. Na przykład aplikacja Kalkulator dostarczana z systemem Windows wykorzystuje teraz arytmetykę o dowolnej precyzji do rozwiązania tego problemu .

MSN
źródło
4

wprowadź opis zdjęcia tutaj

    You can use library https://github.com/MikeMcl/decimal.js/. 
    it will   help  lot to give proper solution. 
    javascript console output 95 *722228.630 /100 = 686117.1984999999
    decimal library implementation 
    var firstNumber = new Decimal(95);
    var secondNumber = new Decimal(722228.630);
    var thirdNumber = new Decimal(100);
    var partialOutput = firstNumber.times(secondNumber);
    console.log(partialOutput);
    var output = new Decimal(partialOutput).div(thirdNumber);
    alert(output.valueOf());
    console.log(output.valueOf())== 686117.1985
Ashish Singhal
źródło
3

Masz rację, przyczyną tego jest ograniczona precyzja liczb zmiennoprzecinkowych. Przechowuj swoje liczby wymierne jako podział na dwie liczby całkowite, aw większości sytuacji będziesz mógł przechowywać liczby bez utraty precyzji. Jeśli chodzi o drukowanie, możesz chcieć wyświetlić wynik jako ułamek. Dzięki reprezentacji, którą zaproponowałem, staje się to banalne.

Oczywiście nie pomoże to w przypadku liczb nieracjonalnych. Ale możesz zoptymalizować swoje obliczenia w taki sposób, aby powodowały najmniejszy problem (np. Wykrywanie takich sytuacji sqrt(3)^2).

Skalee
źródło
Masz rację, powodem tego jest ograniczona precyzja liczb zmiennoprzecinkowych - w <pedant>rzeczywistości OP przypisuje to nieprecyzyjnym operacjom liczb zmiennoprzecinkowych, co jest błędne</pedant>
przypisuje detly
3

Miałem paskudny problem z błędem zaokrąglenia w przypadku mod 3. Czasami, gdy powinienem dostać 0, dostaję .000 ... 01. To dość łatwe w obsłudze, wystarczy przetestować dla <= .01. Ale czasami dostawałem 2.9999999999999998. AUĆ!

BigNumbers rozwiązał problem, ale wprowadził inny, nieco ironiczny problem. Próbując załadować 8,5 do BigNumbers, zostałem poinformowany, że to naprawdę 8.4999… i miał więcej niż 15 cyfr znaczących. Oznaczało to, że BigNumbers nie mógł tego zaakceptować (myślę, że wspominałem, że ten problem był nieco ironiczny).

Proste rozwiązanie ironicznego problemu:

x = Math.round(x*100);
// I only need 2 decimal places, if i needed 3 I would use 1,000, etc.
x = x / 100;
xB = new BigNumber(x);
mcgeo52
źródło
2

Użyj numeru (1.234443) .to Naprawiono (2); wydrukuje 1,23

function test(){
    var x = 0.1 * 0.2;
    document.write(Number(x).toFixed(2));
}
test();
Harish.bazee
źródło
2

decimal.js , big.js lub bignumber.js mogą być używane w celu uniknięcia problemów z obsługą zmiennoprzecinkowych w Javascript:

0.1 * 0.2                                // 0.020000000000000004
x = new Decimal(0.1)
y = x.times(0.2)                          // '0.2'
x.times(0.2).equals(0.2)                  // true

big.js: minimalistyczny; łatwy w użyciu; precyzja określona w miejscach dziesiętnych; precyzja stosowana tylko do podziału.

bignumber.js: bases 2-64; opcje konfiguracji; NaN; Nieskończoność; precyzja określona w miejscach dziesiętnych; precyzja stosowana tylko do podziału; podstawowe prefiksy.

decimal.js: bases 2-64; opcje konfiguracji; NaN; Nieskończoność; potęgi niecałkowite, exp, ln, log; precyzja określona cyframi znaczącymi; precyzja zawsze stosowana; losowe liczby.

link do szczegółowych porównań

Erikas Pliauksta
źródło
2

Elegancki, przewidywalny i wielokrotnego użytku

Zajmijmy się tym problemem w elegancki sposób, który można ponownie wykorzystać. Poniższe siedem wierszy pozwoli ci uzyskać dostęp do żądanej precyzji zmiennoprzecinkowej na dowolnej liczbie, po prostu dołączając .decimalna końcu liczby, formuły lub wbudowanej Mathfunkcji.

// First extend the native Number object to handle precision. This populates
// the functionality to all math operations.

Object.defineProperty(Number.prototype, "decimal", {
  get: function decimal() {
    Number.precision = "precision" in Number ? Number.precision : 3;
    var f = Math.pow(10, Number.precision);
    return Math.round( this * f ) / f;
  }
});


// Now lets see how it works by adjusting our global precision level and 
// checking our results.

console.log("'1/3 + 1/3 + 1/3 = 1' Right?");
console.log((0.3333 + 0.3333 + 0.3333).decimal == 1); // true

console.log(0.3333.decimal); // 0.333 - A raw 4 digit decimal, trimmed to 3...

Number.precision = 3;
console.log("Precision: 3");
console.log((0.8 + 0.2).decimal); // 1
console.log((0.08 + 0.02).decimal); // 0.1
console.log((0.008 + 0.002).decimal); // 0.01
console.log((0.0008 + 0.0002).decimal); // 0.001

Number.precision = 2;
console.log("Precision: 2");
console.log((0.8 + 0.2).decimal); // 1
console.log((0.08 + 0.02).decimal); // 0.1
console.log((0.008 + 0.002).decimal); // 0.01
console.log((0.0008 + 0.0002).decimal); // 0

Number.precision = 1;
console.log("Precision: 1");
console.log((0.8 + 0.2).decimal); // 1
console.log((0.08 + 0.02).decimal); // 0.1
console.log((0.008 + 0.002).decimal); // 0
console.log((0.0008 + 0.0002).decimal); // 0

Number.precision = 0;
console.log("Precision: 0");
console.log((0.8 + 0.2).decimal); // 1
console.log((0.08 + 0.02).decimal); // 0
console.log((0.008 + 0.002).decimal); // 0
console.log((0.0008 + 0.0002).decimal); // 0

Twoje zdrowie!

Bernesto
źródło
2
Jeśli zdecydujesz się głosować, podaj przynajmniej powód.
Bernesto
1

Posługiwać się

var x = 0.1*0.2;
 x =Math.round(x*Math.pow(10,2))/Math.pow(10,2);
Himadri
źródło
4
Hmm ... ale zauważ, że to zawsze zaokrągla do 2 miejsc po przecinku. To oczywiście byłaby opcja, ale co z obliczeniem 0,55 * 0,55 (ponieważ nie znam dokładnych liczb z góry. To dałoby 0,3 zamiast 0,3025. Oczywiście, że mógłbym wtedy użyć Math.round(x*Math.pow(10,4))/Math.pow(10,4);. Zaokrąglanie jest zawsze opcją, ale chciałem tylko wiedzieć, czy istnieje jakieś lepsze rozwiązanie
Juri
10.2 zawsze zwraca 10,19 jsbin.com/tozogiwide/edit?html,js,console,output
Žilvinas
1

To działa dla mnie:

function round_up( value, precision ) { 
    var pow = Math.pow ( 10, precision ); 
    return ( Math.ceil ( pow * value ) + Math.ceil ( pow * value - Math.ceil ( pow * value ) ) ) / pow; 
}

round_up(341.536, 2); // 341.54
Antonio Max
źródło
1
niestety, round_up (4.15,2) => 4.16.
jrg
1

Dane wyjściowe przy użyciu następującej funkcji:

var toFixedCurrency = function(num){
    var num = (num).toString();
    var one = new RegExp(/\.\d{1}$/).test(num);
    var two = new RegExp(/\.\d{2,}/).test(num);
    var result = null;

    if(one){ result = num.replace(/\.(\d{1})$/, '.$10');
    } else if(two){ result = num.replace(/\.(\d{2})\d*/, '.$1');
    } else { result = num*100; }

    return result;
}

function test(){
    var x = 0.1 * 0.2;
    document.write(toFixedCurrency(x));
}

test();

Zwróć uwagę na wynik toFixedCurrency(x).

Júlio Paulillo
źródło