Поймай хвостик данных — ловушка JS с плавающей запятой и решение

JavaScript анализ данных
Поймай хвостик данных — ловушка JS с плавающей запятой и решение

Первоначально из колонки Чжиху:zhuanlan.zhihu.com/ne-fe

Как мы все знаем, операции с числами с плавающей запятой в JavaScript часто0.000000001а также0.999999999Такие странные результаты, как0.1+0.2=0.30000000000000004,1-0.9=0.09999999999999998Многие люди знают, что это проблема с плавающей точкой, но конкретная причина не ясна. Эта статья поможет вам уточнить принципы и решения позади этого, и объяснит ямы, которые встретятся в JS в JS.

хранение чисел с плавающей запятой

Первое, что нужно сделать, это выяснить, как JavaScript хранит десятичные дроби. В отличие от других языков, таких как Java и Python, все числа в JavaScript, включая целые и десятичные дроби, имеют только один тип —Number. Его реализация следуетIEEE 754Стандартный, использующий 64-битное представление с фиксированной длиной, то есть стандартные числа с плавающей запятой двойной двойной точности (связанные с плавающими 32-битными числами одинарной точности). Подробно освещены принципы компьютерной композиции, если не помните, ничего страшного.

Примечание: десятичные дроби в большинстве языков по умолчанию соответствуют числам с плавающей запятой, совместимым с IEEE 754, включая Java, Ruby и Python.Ошибки с плавающей запятой, описанные в этой статье, также существуют.

Преимущество такой структуры хранения заключается в том, что целые и десятичные числа можно нормализовать для экономии места для хранения.

64-битные биты можно разделить на три части:

  • Знаковый бит S: первый бит — это знаковый бит (знак) положительных и отрицательных чисел, 0 для положительных чисел и 1 для отрицательных чисел.
  • Бит экспоненты E: Средние 11 бит хранят экспоненту (экспоненту), которая используется для представления мощности.
  • Мантисса M: последние 52 бита — это мантисса, а лишняя часть автоматически округляется до нуля.

64 bit allocation
64 bit allocation

Фактическое число может быть рассчитано по следующей формуле:

数字计算公式
цифровая формула расчета

Обратите внимание, что приведенная выше формула соответствует норме научной записи в десятичном формате.0<M<10, в двоичном виде0<M<2. То есть целая часть может быть только 1, поэтому ее можно округлить, и останется только дробная часть. Такие как4.5Преобразованный в двоичный файл100.1, в научной записи1.001*2^2, после округления 1M = 001. E是一个无符号整数,因为长度是11位,取值范围是 0~2047。但是科学计数法中的指数是可以为负数的,所以约定减去一个中间数 1023,[0,1022]представлен как отрицательный,[1024,2047]выражен как положительный. показатели вроде 4,5E = 1025, мантиссаM = 001.

Окончательная формула становится:

最终计算公式
формула окончательного расчета

так4.5Окончательное представление (M=001, E=1025):

4.5 allocation map
4.5 allocation map

(изображение создано из этогоwoohoo.binary convert.com/convert_both…)

Следующий0.1В качестве примера, чтобы объяснить причины ошибок с плавающей запятой,0.1Преобразуется в двоичное представление как0.0001100110011001100(1100 циклов),1.100110011001100x2^-4,такE=-4+1023=1019;M отбрасывает ведущую 1, чтобы получить100110011.... В итоге это:

0.1 allocation map
0.1 allocation map

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

Зачем0.1+0.2=0.30000000000000004?

Этапы расчета:

// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111

// 转成十进制正好是 0.30000000000000004

Зачемx=0.1может получить0.1?

Поздравляю, вы достигли области, где вы можете видеть, что гора не гора. Поскольку фиксированная длина мантиссы составляет 52 бита плюс пропущенный один бит, максимальное число, которое может быть представлено, равно2^53=9007199254740992, соответствующая мантисса в экспоненциальном представлении равна9.007199254740992, что является максимальной точностью, которую может представить JS. Его длина равна 16, поэтому его можно аппроксимировать с помощьюtoPrecision(16)Для выполнения точных операций избыточная точность будет автоматически округлена в большую сторону. Итак, есть:

0.10000000000000000555.toPrecision(16)
// 返回 0.1000000000000000,去掉末尾的零后正好为 0.1

// 但你看到的 `0.1` 实际上并不是 `0.1`。不信你可用更高的精度试试:
0.1.toPrecision(21) = 0.100000000000000005551

Кризис Тарса

Может быть, вы почувствовали, если целое число больше 9007199254740992?
Поскольку максимальное значение E равно 1023, наибольшее целое число, которое может быть представлено, равно2^1024 - 1, что является наибольшим целым числом, которое может быть представлено. Но вы не можете вычислить число таким образом, потому что из2^1024стал в началеInfinity

> Math.pow(2, 1023)
8.98846567431158e+307

> Math.pow(2, 1024)
Infinity

тогда для(2^53, 2^63)Что произойдет с числами между ними?

  • (2^53, 2^54)Число между выберет одно из двух, только четные числа могут быть представлены точно
  • (2^54, 2^55)Число между ними выберет одно из четырех и может представлять ровно 4 кратных.
  • ... пропустить больше кратных 2 по очереди

Следующая картинка хорошо иллюстрирует соответствие между числами с плавающей запятой и действительными числами в JavaScript. наш обычный(-2^53, 2^53)Это лишь очень маленькая часть посередине, и более разреженная и неточная она идет к двум сторонам.

fig1.jpg
fig1.jpg

В ранней системе заказов Taobao номер заказа рассматривался как число, а затем номер случайного заказа резко увеличился, что превысило
9007199254740992, окончательное решение состоит в том, чтобы преобразовать номер заказа в строку для обработки.

Для решения проблемы больших чисел можно обратиться к сторонней библиотекеbignumber.js, принцип состоит в том, чтобы рассматривать все числа как строки и заново реализовать логику вычислений.Недостаток в том, что производительность намного хуже, чем нативная, поэтому необходимо нативно поддерживать большие числа. У TC39 уже есть предложение Этапа 3proposal bigint, ожидается, что большое количество проблем будет полностью решено. Прежде чем браузер официально поддержит его, его можно реализовать с помощью Babel 7.0, который автоматически конвертируется вbig-integerДля расчета это может сохранить точность, но эффективность работы будет снижена.

toPrecision vs toFixed

При обработке данных эти две функции легко спутать. Их объединяет преобразование чисел в строки для отображения. Обратите внимание, что он не используется в середине расчета, а только для конечного результата.

Необходимо отметить отличия:

  • toPrecision— точность обработки, и точность отсчитывается от первого ненулевого числа слева направо.
  • toFixedОкругляется до указанного количества знаков после запятой, начиная с запятой.

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

Такие как:1.005.toFixed(2)то, что возвращается1.00вместо1.01.

причина:1.005Фактическая соответствующая цифра1.00499999999999989, которые все округляются при округлении!

Решение: используйте функцию округленияMath.round()обрабатывать. ноMath.round(1.005 * 100) / 100еще нет, потому что1.005 * 100 = 100.49999999999999. Также необходимо решить ошибки точности умножения и деления перед использованиемMath.round. Вы можете использовать следующиеnumber-precision#roundметод решения.

решение

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

Класс отображения данных

когда вы получаете1.4000000000000001Когда такие данные должны быть отображены, рекомендуется использоватьtoPrecisionокруглятьparseFloatПреобразуйте его в число, а затем отобразите следующим образом:

parseFloat(1.4000000000000001.toPrecision(12)) === 1.4  // True

Метод инкапсуляции:

function strip(num, precision = 12) {
  return +parseFloat(num.toPrecision(precision));
}

Почему вы выбираете12как точность по умолчанию? Это эмпирический выбор.Как правило, выбор 12 может решить большую часть задач 0001 и 0009, и в большинстве случаев этого достаточно.Если вам нужна большая точность, вы можете увеличить ее.

Класс операций с данными

Для арифметических операций, таких как+-*/, вы не можете использоватьtoPrecision. Правильный способ - преобразовать десятичную дробь в целое число, а затем действовать. В качестве примера возьмем дополнение:

/**
 * 精确加法
 */
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.3e+1(Когда числовая точность превышает 21, число будет принудительно отображаться в экспоненциальном представлении) и требуется особая обработка.

Если вы можете прочитать это, это показывает, что вы очень терпеливы, поэтому я поставлю бонус. Его можно использовать непосредственно при обнаружении ошибок с плавающей запятой.
GitHub.com/but he-share/num be…

Отлично поддерживает сложение, вычитание, умножение и деление чисел с плавающей запятой, округление и другие операции. Очень маленький, всего 1 КБ, намного меньше, чем большинство подобных библиотек (таких как Math.js, BigDecimal.js), 100% полное тестовое покрытие, хорошая читабельность кода, вы также можете использовать его в своем приложении!

Ссылаться на

Если вы считаете, что эта статья поможет вам, коснитесь лайка, чтобы поощрить ее.