8

Как решить проблемы с точностью чисел с плавающей запятой в JavaScript? [повтор]

1

Описание проблемы:

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

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

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

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

Конечно, в противном случае я округлю до 10 знаков или около того.

5 ответ(ов)

0

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

0

Действительно, удивительно, что эта функция еще не была опубликована, хотя существуют похожие её вариации. Она взята из документации 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 была обновлена, поэтому, возможно, кто-то сможет предложить лучшее решение.

0

Данный код демонстрирует способы выполнения арифметических операций с числами с плавающей запятой и округления результатов до двух знаков после запятой. Рассмотрим несколько функций:

  1. Функция times:
var times = function (a, b) {
    return Math.round((a * b) * 100)/100;
};

Эта функция принимает два аргумента a и b, перемножает их и округляет результат до двух знаков после запятой. Например, вызов times(0.1, 0.2) вернёт 0.02.

  1. Функция fpFix:
var fpFix = function (n) {
    return Math.round(n * 100)/100;
};

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

Эта функция предназначена для округления любого числа n до двух знаков после запятой. Она может быть использована для округления результата умножения 0.1 * 0.2.

  1. Функция 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

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

0

Если вы хотите обойти проблему с точностью при выполнении арифметических операций с плавающей запятой, вы можете использовать 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(), мы можем избежать ошибок округления и получить корректные значения.

0

Вам просто нужно решить, сколько знаков после запятой вы действительно хотите получить — как говорится, "нельзя и лайкать, и одновременно пожинать плоды" 😃

Числовые ошибки накапливаются с каждой новой операцией, и если вы не обрежете их на раннем этапе, они будут только увеличиваться. Числовые библиотеки, которые представляют результаты в аккуратном виде, просто обрезают последние 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).

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь