Как решить проблемы с точностью чисел с плавающей запятой в JavaScript? [повтор]
Описание проблемы:
У меня есть следующий тестовый скрипт:
function test() {
var x = 0.1 * 0.2;
document.write(x);
}
test();
Этот код выводит результат 0.020000000000000004
, хотя должен показывать просто 0.02
(если использовать калькулятор). Насколько я понял, это связано с ошибками точности при умножении с плавающей точкой.
Есть ли у кого-то хорошее решение, чтобы в таком случае получить правильный результат 0.02
? Я знаю, что существуют такие функции, как toFixed
, или можно использовать округление, но мне бы хотелось, чтобы число выводилось полностью, без обрезания и округления. Хотелось бы узнать, есть ли у кого-то элегантное решение для этой проблемы.
Конечно, в противном случае я округлю до 10 знаков или около того.
5 ответ(ов)
Вы только выполняете умножение? Если да, то вы можете воспользоваться интересным правилом десятичной арифметики. Дело в том, что правило КоличествоДесятичных(X) + КоличествоДесятичных(Y) = ОжидаемоеКоличествоДесятичных
действительно. Например, если у нас есть 0.123 * 0.12
, то мы знаем, что итоговое число будет иметь 5 знаков после запятой, так как 0.123
имеет 3 знака, а 0.12
- 2. Таким образом, если JavaScript вернет нам число вроде 0.014760000002
, мы можем без опасений округлить его до 5-го знака после запятой, не потеряв точность.
Действительно, удивительно, что эта функция еще не была опубликована, хотя существуют похожие её вариации. Она взята из документации MDN для Math.round()
. Функция компактна и позволяет задавать различную точность.
Вот сама функция:
function precisionRound(number, precision) {
var factor = Math.pow(10, precision);
return Math.round(number * factor) / factor;
}
console.log(precisionRound(1234.5678, 1)); // ожидаемый результат: 1234.6
console.log(precisionRound(1234.5678, -1)); // ожидаемый результат: 1230
Для использования в веб-разработке, вот пример с HTML и обработкой события кнопки:
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 precisionRound(number, precision) {
var factor = Math.pow(10, precision);
return Math.round(number * factor) / factor;
}
И соответствующий CSS:
button {
display: block;
}
А вот и форма:
<input type='text' value='0.1'>
<input type='text' value='0.2'>
<button>Получить произведение</button>
<input type='text'>
ОБНОВЛЕНИЕ: 20 августа 2019
Недавно заметил ошибку. Я полагаю, она связана с ошибкой точности с плавающей запятой, возникающей при использовании Math.round()
.
precisionRound(1.005, 2); // выдает 1, что неверно, должно быть 1.01
Следующие условия работают корректно:
precisionRound(0.005, 2); // выдает 0.01
precisionRound(1.0005, 3); // выдает 1.001
precisionRound(1234.5, 0); // выдает 1235
precisionRound(1234.5, -1); // выдает 1230
Исправление:
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;
}
Это изменение добавляет цифру вправо при округлении десятичных значений. Страница Math.round()
на MDN была обновлена, поэтому, возможно, кто-то сможет предложить лучшее решение.
Данный код демонстрирует способы выполнения арифметических операций с числами с плавающей запятой и округления результатов до двух знаков после запятой. Рассмотрим несколько функций:
- Функция
times
:
var times = function (a, b) {
return Math.round((a * b) * 100)/100;
};
Эта функция принимает два аргумента a
и b
, перемножает их и округляет результат до двух знаков после запятой. Например, вызов times(0.1, 0.2)
вернёт 0.02
.
- Функция
fpFix
:
var fpFix = function (n) {
return Math.round(n * 100)/100;
};
fpFix(0.1*0.2); // -> 0.02
Эта функция предназначена для округления любого числа n
до двух знаков после запятой. Она может быть использована для округления результата умножения 0.1 * 0.2
.
- Функция
fpArithmetic
:
var fpArithmetic = function (op, x, y) {
var n = {
'*': x * y,
'-': x - y,
'+': x + y,
'/': x / y
}[op];
return Math.round(n * 100)/100;
};
Эта функция более универсальна и принимает оператор op
, а также два числа x
и y
. Она выполняет арифметическую операцию в зависимости от переданного оператора и также округляет результат до двух знаков после запятой.
Примеры использования:
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
Таким образом, все три функции удобно используются для работы с числами с плавающей запятой, обеспечивая округление результатов вычислений.
Если вы хотите обойти проблему с точностью при выполнении арифметических операций с плавающей запятой, вы можете использовать parseFloat()
и toFixed()
. Вот пример:
a = 0.1;
b = 0.2;
a + b = 0.30000000000000004; // Из-за особенностей представления чисел с плавающей запятой
c = parseFloat((a + b).toFixed(2)); // Округляем до 2 знаков после запятой
c = 0.3; // Теперь результат корректный
a = 0.3;
b = 0.2;
a - b = 0.09999999999999998; // Опять же, проблема с точностью
c = parseFloat((a - b).toFixed(2)); // Округляем результат
c = 0.1; // Теперь всё правильно
Используя toFixed()
, мы можем избежать ошибок округления и получить корректные значения.
Вам просто нужно решить, сколько знаков после запятой вы действительно хотите получить — как говорится, "нельзя и лайкать, и одновременно пожинать плоды" 😃
Числовые ошибки накапливаются с каждой новой операцией, и если вы не обрежете их на раннем этапе, они будут только увеличиваться. Числовые библиотеки, которые представляют результаты в аккуратном виде, просто обрезают последние 2 знака на каждом шаге. То же самое делают числовые сопроцессоры: у них есть "нормальная" и "полная" длина по той же причине. Обрезка — это недорогая операция для процессора, но она может оказаться дорогостоящей для вас в скрипте (при умножении, делении и использовании pov(...)). Хорошая математическая библиотека предоставит функцию floor(x,n) для выполнения обрезки за вас.
Поэтому, как минимум, вам следует создать глобальную переменную или константу с помощью pov(10,n), что означает, что вы определились с необходимой точностью 😃 Затем сделайте следующее:
Math.floor(x * PREC_LIM) / PREC_LIM // floor - здесь вы обрезаете, а не округляете
Вы также можете продолжать выполнять математические операции и обрезать только в конце — при условии, что вы только отображаете результаты и не используете их в условных операторах. Если так, то использование .toFixed(...) может оказаться более эффективным.
Если вы делаете условные операторы или сравнения и не хотите обрезать, вам также понадобится небольшая константа, обычно называемая eps, которая имеет одно десятичное место выше, чем максимальная ожидаемая ошибка. Допустим, ваша обрезка происходит на последних двух знаках — тогда ваша eps будет иметь 1 на третьем месте с конца (третьем наименее значимом), и вы можете использовать её для сравнения, находится ли результат в пределах диапазона eps относительно ожидаемого значения (0.02 - eps < 0.1 * 0.2 < 0.02 + eps).
Форматирование числа с обязательным отображением 2 знаков после запятой
В чем разница между String.slice и String.substring?
Проверка соответствия строки регулярному выражению в JS
Как использовать десятичное значение шага в range()?
Как создать диалог с кнопками "Ок" и "Отмена"