-2

Есть задача реализовать вычитания value между двумя полями. И выводить результат при введении. Задача реализована по этой ссылке:

http://jack-dev.zzz.com.ua/calculate/

Оказываемся ввиду особенностей JS исторических, есть баги с десятичными дробями которые тянутся издавна. Пример:

image

Подскажите варианты обхода этого, регулярками возможно или с помощью класса Math. Что бы пользователь не ввёл после запятой (любое кол-во цифр), чтобы всегда результат был корректным. И что ещё можно учесть, какие особенности работы с дробями в JS?

2 Answers2

8

Нет никаких багов с десятичными дробями, но есть особенности представления вещественных чисел в памяти компьютера. Грубо говоря: вещественное число, которое записывается конечной последовательностью десятичных знаков после запятой в десятеричном исчислении, может иметь бесконечную последовательность двоичных знаков после запятой в двоичном исчислении... И будет округлено до ближайшего значения с определенной точностью, и, соответсвенно, эту точность потеряет и в десятеричном исчислении.

Числа с плавающей точкой в JS имеют двойную точность, и содержат как минимум 15 значащих цифр (а как максимум 17 значащих цифр, включая десятичные нули). Для того, чтобы числа выглядели "красиво", достаточно округлять их до 14 значащих цифр, то есть на единицу меньше минимального количества значащих цифр (так как погрешность может содержаться только в цифрах начиная с 15-ой).

Таким образом, для чисел в интервале (-1, 1) для округления можно использовать функцию toFixed с аргументом 14.

var a = 0.1 + 0.2; // a = 0.30000000000000004

alert(a.toFixed(14)); // вывод округленного числа: 0.3

Такое округление следует делать только в момент вывода на экран. Все расчеты должны выполняться без округления. Иначе с каждым вычислением у вас будет дополнительно накапливаться погрешность от округления.

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

var b = 32123.1 - 0.2; // b = 32122.899999999998

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

function toFixed(value) {
  var power = Math.pow(10, 14);
  return String(Math.round(value * power) / power);
}

var a = 0.1 + 0.2; // a = 0.30000000000000004
var b = 32123.1 - 0.2; // b = 32122.899999999998

alert(toFixed(a)); // вывод округленного числа: 0.3 alert(toFixed(b)); // вывод округленного числа: 32122.9

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

NoSkill
  • 2,304
  • 1
  • 7
  • 15
  • Обьясните пожалуйста, почему toFixed именно 14, а не 10 или 19 – Давид Манжула Jul 05 '20 at 20:24
  • Написано все, внимательно почитайте... Добавил еще и ссылку. – NoSkill Jul 05 '20 at 20:26
  • Мой косяк, спасибо – Давид Манжула Jul 05 '20 at 20:29
  • Спасибо за развернутый ответ. – Jack Kosovskiy Jul 11 '20 at 11:24
  • 1
    Но как быть с вычитанием и в больших числах с дробью? например

    let a = 0.3 - 0.1; console.log(a.toFixed(14));

    let b = 32123.1 - 0.2; console.log(b.toFixed(14));

    – Jack Kosovskiy Jul 11 '20 at 11:36
  • @Jack Kosovskiy Спасибо за коментарий! Дополнил ответ! – NoSkill Jul 11 '20 at 16:02
  • А что если мне нужно округление в середине вычислений, как тогда быть? ну вот например, мне нужно высчитать такое выражение (описал на псевдо-языке): round(a * b, 2) + round (c * d, 3) * round(e, 3), где round(a, x) - функция округляющая число a до точности x? – Andrei Khotko Apr 17 '23 at 07:00
  • @AndreiKhotko это другой вопрос: function round(value, precision) { var power = Math.pow(10, precision); return Math.round((value + Number.EPSILON) * power) / power; } И ответ на него есть, например, здесь: https://stackoverflow.com/questions/11832914/how-to-round-to-at-most-2-decimal-places-if-necessary – NoSkill Apr 17 '23 at 19:44
1

console.log(0.1 + 0.2);
console.log(Number((0.1 + 0.2).toFixed(14)));