С эволюцией моделей потребления онлайн-покупки стали неотъемлемым аспектом общественной жизни. Обеспечение безопасности потребителей, конфиденциальности пользователей и точности имеют первостепенное значение. Представьте себе ситуацию: пользователь совершает покупку, но сумма оплаты неверна. Как отреагирует пользователь? (WTF?) Отрицательный отзыв неизбежен. Это не только снижает лояльность пользователей, но и затрудняет удержание клиентов. Когда руководство компании узнает о таком пользовательском опыте, программист, ответственный за код, может оказаться в центре внимания, столкнувшись с негативными последствиями. Как опытный разработчик, я давно столкнулся с проблемой точности вычислений, но откладывал ее решение. Однако, когда кто-то недавно обратился ко мне с соответствующими вопросами, я понял, как важно углубиться в эту тему и поделиться своими мыслями о достижении точных вычислений в JavaScript.

Вычисление ролловеров с помощью JavaScript

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

Хотя это может показаться тривиальным примером, он выявляет распространенную проблему в вычислениях JavaScript. Наша цель состоит не только в том, чтобы сосредоточиться на этом конкретном примере, но и в том, чтобы понять, почему возникают такие результаты. Какой шаг был неправильным? Существуют ли другие расчеты, которые могут столкнуться с подобными проблемами? И самое главное, как мы можем эффективно с ними справляться?

Как JavaScript представляет числа?

В JavaScript числа (как целые числа, так и числа с плавающей запятой) представляются с использованием типа Number. JavaScript следует стандарту IEEE 754, который использует 64 бита для представления числа.

Вот разбивка того, как используются эти 64 бита:

  • Бит 0: бит знака, где 0 представляет положительное число, а 1 представляет отрицательное число.
  • Биты с 1 по 11: часть экспоненты, хранящая значение экспоненты.
  • Биты с 12 по 63: дробная часть (также известная как действующее число), в которой хранятся значащие цифры. (мантисса)

Теперь углубимся в интересный факт: максимально безопасное число в JavaScript представлено числом.MAX_SAFE_INTEGER, что равно Math.pow(2, 53) — 1, а не Math.pow(2, 52) — 1. Но почему что по делу? Ведь мантисса состоит всего из 52 бит, верно?

Причина в том, что двоичное представление всегда принимает вид 1.xx…xx, где первая цифра мантиссы в общепринятой записи считается равной 1 (таким образом, она опущена и явно не записывается). Xx…xx представляет мантиссу f и может состоять из 52 цифр. Таким образом, JavaScript обеспечивает допустимое числовое представление с точностью до 53 двоичных разрядов (последние 52 бита 64-битного числа с плавающей запятой плюс пропущенный 1 бит).

Просто проверьте -

Что происходит в процессе расчета?

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

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

По этой идее анализируем процесс работы 0.1+0.2

1. Шестнадцатеричное преобразование

0.1 и 0.2 будут бесконечным циклом после преобразования в двоичный

0.1 -> 0.0001100110011001...(infinite loop)
0.2 -> 0.0011001100110011...(infinite loop)

Однако из-за ограничения количества цифр в мантиссе стандарта IEEE 754 необходимо обрезать лишние биты сзади (данная статья использует этот веб-сайт, чтобы наглядно показать двоичное представление чисел с плавающей запятой в памяти)

0.1

0.2

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

Вот еще один небольшой пункт знаний

Тогда почему x=0,1 может получить 0,1?

Это потому, что это 0,1 на самом деле не 0,1. Разве это не бессмысленно? успокойся, позволь мне объяснить

Стандарт предусматривает, что фиксированная длина мантиссы f составляет 52 бита плюс один пропущенный бит, эти 53 бита являются диапазоном точности JS. Он может представлять до 2⁵³ (9007199254740992), а длина равна 16, поэтому вы можете использовать toPrecision(16) для выполнения точных вычислений, а избыточная точность будет автоматически округлена в большую сторону.

0.10000000000000000555.toPrecision(16)
// returns 0.1000000000000000, which is exactly 0.1 after removing the trailing zero
0.1.toPrecision(21) = 0.100000000000000005551

Вот почему 0,1 может быть равно 0,1. хорошо давай

2. Параллельная работа

Из-за разницы в количестве показателей операцию необходимо выполнять со стороны операции порядка, что также может привести к потере точности.

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

0.0100110011001100110011001100110011001100110011001100

После преобразования результата в десятичный вид он равен 0,300000000000000004, поэтому выполняется предыдущая операция: 0,1 + 0,2 != 0,3

so:

В процессе конвертации базы и работы с заказом может произойти потеря точности

Как решить проблему точности?

1. Преобразование чисел в целые числа

Это самый простой способ думать об этом, и это относительно просто

function add(num1, num2) {
 const num1Digits = (num1.toString().split('.')[1] || '').length;
 const num2Digits = (num2.toString().split('.')[1] || '').length;
 const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
 return (num1 * baseNum + num2 * baseNum) / baseNum;
}

Но этот метод по-прежнему не годится для больших чисел.

2. Трехсторонняя библиотека

Это более комплексный подход. Я рекомендую 2 библиотеки, с которыми я обычно соприкасаюсь.

1).Math.js

Обширная математическая библиотека, посвященная JavaScript и Node.js. Поддерживает числа, большие числа (числа за безопасными числами), комплексные числа, дроби, единицы измерения и матрицы. Мощный и простой в использовании.

Официальный сайт: mathjs.org/

2).big.js

Официальный сайт: mikemcl.github.io/big.js

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

Вышеизложенное является моим пониманием точного расчета js, и я надеюсь, что оно может вам помочь.