Как бороться с точностью чисел с плавающей точкой в ​​JavaScript? - PullRequest
511 голосов
/ 22 сентября 2009

У меня есть следующий фиктивный скрипт:

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

Это напечатает результат 0.020000000000000004, тогда как он должен просто напечатать 0.02 (если вы используете свой калькулятор). Насколько я понял, это связано с ошибками в точности умножения с плавающей запятой.

У кого-нибудь есть хорошее решение, чтобы в таком случае я получил правильный результат 0.02? Я знаю, что есть такие функции, как toFixed, иначе возможна округление, но я бы хотел напечатать целое число без обрезки и округления Просто хотел узнать, есть ли у кого-нибудь хорошее, элегантное решение.

Конечно, иначе я округлю до 10 или около того цифр.

Ответы [ 34 ]

424 голосов
/ 09 августа 2010

Из справочника с плавающей точкой :

Что я могу сделать, чтобы избежать этой проблемы?

Это зависит от того, какого рода расчеты вы делаете.

  • Если вам действительно нужны ваши результаты, чтобы сложить точно, особенно когда вы работа с деньгами: используйте специальный десятичный Тип данных.
  • Если вы просто не хотите видеть все эти дополнительные десятичные разряды: просто отформатируйте результат с округлением до фиксированного количество десятичных знаков при показывая его.
  • Если у вас нет доступного десятичного типа данных, альтернативой является работа с целыми числами, например делать деньги расчеты целиком в центах. Но это больше работы и имеет некоторые недостатки.

Обратите внимание, что первая точка применяется только в том случае, если вам действительно нужно определенное точное десятичное поведение. Большинству людей это не нужно, они просто раздражены тем, что их программы не работают правильно с числами, такими как 1/10, даже не подозревая, что они не будут мигать при той же ошибке, если это произошло с 1/3 *

Если первая точка зрения действительно применима к вам, используйте BigDecimal для JavaScript , что совсем не элегантно, но фактически решает проблему, а не предоставляет несовершенный обходной путь.

108 голосов
/ 05 сентября 2010

Мне нравится решение Педро Ладарии и я использую что-то похожее.

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

В отличие от решения Pedros, оно округляется до 0,999 ... повторяется и с точностью до плюс / минус один на младшей значащей цифре.

Примечание. При работе с 32- или 64-разрядными числами с плавающей запятой следует использовать toPrecision (7) и toPrecision (15) для достижения наилучших результатов. См. этот вопрос , чтобы узнать почему.

64 голосов
/ 20 сентября 2013

Для математически наклонного: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html

Рекомендуется использовать поправочные коэффициенты (умножить на подходящую степень 10, чтобы арифметика получалась между целыми числами) Например, в случае 0.1 * 0.2 поправочный коэффициент равен 10, и вы выполняете вычисление:

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

(очень быстрое) решение выглядит примерно так:

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); };

В этом случае:

> Math.m(0.1, 0.2)
0.02

Я определенно рекомендую использовать проверенную библиотеку, такую ​​как SinfulJS

41 голосов
/ 11 августа 2010

Вы только выполняете умножение? Если так, то вы можете использовать в своих интересах секрет секретной десятичной арифметики. Это то, что NumberOfDecimals(X) + NumberOfDecimals(Y) = ExpectedNumberOfDecimals. То есть, если у нас есть 0.123 * 0.12, то мы знаем, что будет 5 десятичных знаков, потому что 0.123 имеет 3 десятичных знака, а 0.12 имеет два. Таким образом, если JavaScript дал нам число типа 0.014760000002, мы можем безопасно округлить до 5-го знака после запятой, не боясь потерять точность.

24 голосов
/ 06 августа 2010

Вы ищете реализацию sprintf для JavaScript, чтобы вы могли выписывать числа с небольшими ошибками (поскольку они хранятся в двоичном формате) в ожидаемом формате.

Попробуйте javascript-sprintf , вы бы назвали это так:

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

чтобы распечатать ваш номер в виде числа с плавающей запятой с двумя десятичными знаками.

Вы также можете использовать Number.toFixed () для отображения, если вы не хотите включать больше файлов просто для округления с плавающей запятой с заданной точностью.

18 голосов
/ 02 декабря 2014

Я нахожу BigNumber.js соответствует моим потребностям.

Библиотека JavaScript для десятичной и недесятичной арифметики произвольной точности.

Имеет хорошую документацию , и автор очень старательно реагирует на отзывы.

У того же автора есть 2 другие похожие библиотеки:

Big.js

Небольшая, быстрая библиотека JavaScript для десятичной арифметики произвольной точности. Младшая сестра bignumber.js.

и Decimal.js

Десятичный тип произвольной точности для JavaScript.

Вот код, использующий 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>
14 голосов
/ 08 августа 2010

Эта функция будет определять необходимую точность по умножению двух чисел с плавающей запятой и возвращать результат с соответствующей точностью. Элегантно, хотя это не так.

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); 
}
14 голосов
/ 08 августа 2010
var times = function (a, b) {
    return Math.round((a * b) * 100)/100;
};

--- или ---

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

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

--- также ---

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

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

--- как в ---

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
11 голосов
/ 06 августа 2010

Вы просто должны решить, сколько десятичных цифр вы на самом деле хотите - не можете съесть торт и съесть его тоже: -)

Числовые ошибки накапливаются при каждой последующей операции, и если вы не отрежете ее раньше, она будет только расти. Числовые библиотеки, которые представляют результаты, которые выглядят чистыми, просто обрезают последние 2 цифры на каждом шаге, числовые сопроцессоры также имеют «нормальную» и «полную» длину по той же причине. Вырезание обходится дешево для процессора, но очень дорого для вас в сценарии (умножение, деление и использование pov (...)). Хорошая математическая библиотека предоставит слово (x, n), чтобы сделать отсечение для вас.

Так что, по крайней мере, вы должны сделать глобальную переменную / константу с помощью pov (10, n) - это означает, что вы определились с точностью, которая вам нужна :-) Затем выполните:

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

Вы также можете продолжать делать математику и только обрезать в конце - при условии, что вы только отображаете, а не делаете if-s с результатами. Если вы можете сделать это, тогда .toFixed (...) может быть более эффективным.

Если вы выполняете сравнения if-s / и не хотите сокращать их, вам также понадобится небольшая константа, обычно называемая eps, которая на один десятичный знак выше максимальной ожидаемой ошибки. Скажем, что ваш предел составляет последние два знака после запятой - тогда ваш eps имеет 1 на 3-м месте от последнего (3-е наименее значимое), и вы можете использовать его, чтобы сравнить, находится ли результат в пределах ожидаемого диапазона eps (0,02 -eps <0,1) * 0,2 <0,02 + EPS). </p>

9 голосов
/ 01 марта 2012

Функция round () на phpjs.org работает хорошо: http://phpjs.org/functions/round

num = .01 + .06;  // yields 0.0699999999999
rnum = round(num,12); // yields 0.07
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...