Округление
Одна из часто используемых операций при работе с числами – это округление.
В JavaScript есть несколько встроенных функций для работы с округлением:Math.floorОкругление в меньшую сторону: 3.1 становится 3, а -1.1 — -2.Math.ceilОкругление в большую сторону: 3.1 становится 4, а -1.1 — -1.Math.roundОкругление до ближайшего целого: 3.1 становится 3, 3.6 — 4, а -1.1 — -1.Math.trunc (не поддерживается в Internet Explorer)Производит удаление дробной части без округления: 3.1 становится 3, а -1.1 — -1.
Ниже представлена таблица с различиями между функциями округления:
Math.floor | Math.ceil | Math.round | Math.trunc | |
|---|---|---|---|---|
3.1 | 3 | 4 | 3 | 3 |
3.6 | 3 | 4 | 4 | 3 |
-1.1 | -2 | -1 | -1 | -1 |
-1.6 | -2 | -1 | -2 | -1 |
Эти функции охватывают все возможные способы обработки десятичной части. Что если нам надо округлить число до n-ого количества цифр в дробной части?
Например, у нас есть 1.2345 и мы хотим округлить число до 2-х знаков после запятой, оставить только 1.23.
Есть два пути решения:
- Умножить и разделить.Например, чтобы округлить число до второго знака после запятой, мы можем умножить число на
100, вызвать функцию округления и разделить обратно.let num = 1.23456; alert( Math.floor(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23 - Метод toFixed(n) округляет число до
nзнаков после запятой и возвращает строковое представление результата.let num = 12.34; alert( num.toFixed(1) ); // "12.3"Округляет значение до ближайшего числа, как в большую, так и в меньшую сторону, аналогично методуMath.round:let num = 12.36; alert( num.toFixed(1) ); // "12.4"Обратите внимание, что результатомtoFixedявляется строка. Если десятичная часть короче, чем необходима, будут добавлены нули в конец строки:let num = 12.34; alert( num.toFixed(5) ); // "12.34000", добавлены нули, чтобы получить 5 знаков после запятойМы можем преобразовать полученное значение в число, используя унарный оператор+илиNumber(), пример с унарным оператором:+num.toFixed(5).
Неточные вычисления
Внутри JavaScript число представлено в виде 64-битного формата IEEE-754. Для хранения числа используется 64 бита: 52 из них используется для хранения цифр, 11 из них для хранения положения десятичной точки (если число целое, то хранится 0), и один бит отведён на хранение знака.
Если число слишком большое, оно переполнит 64-битное хранилище, JavaScript вернёт бесконечность:
alert( 1e500 ); // Infinity
Наиболее часто встречающаяся ошибка при работе с числами в JavaScript – это потеря точности.
Посмотрите на это (неверное!) сравнение:
alert( 0.1 + 0.2 == 0.3 ); // false
Да-да, сумма 0.1 и 0.2 не равна 0.3.
Странно! Что тогда, если не 0.3?
alert( 0.1 + 0.2 ); // 0.30000000000000004
Ой! Здесь гораздо больше последствий, чем просто некорректное сравнение. Представьте, вы делаете интернет-магазин и посетители формируют заказ из 2-х позиций за $0.10 и $0.20. Итоговый заказ будет $0.30000000000000004. Это будет сюрпризом для всех.
Но почему это происходит?
Число хранится в памяти в бинарной форме, как последовательность бит – единиц и нулей. Но дроби, такие как 0.1, 0.2, которые выглядят довольно просто в десятичной системе счисления, на самом деле являются бесконечной дробью в двоичной форме.
Другими словами, что такое 0.1? Это единица делённая на десять — 1/10, одна десятая. В десятичной системе счисления такие числа легко представимы, по сравнению с одной третьей: 1/3, которая становится бесконечной дробью 0.33333(3).
Деление на 10 гарантированно хорошо работает в десятичной системе, но деление на 3 – нет. По той же причине и в двоичной системе счисления, деление на 2 обязательно сработает, а 1/10 становится бесконечной дробью.
В JavaScript нет возможности для хранения точных значений 0.1 или 0.2, используя двоичную систему, точно также, как нет возможности хранить одну третью в десятичной системе счисления.
Числовой формат IEEE-754 решает эту проблему путём округления до ближайшего возможного числа. Правила округления обычно не позволяют нам увидеть эту «крошечную потерю точности», но она существует.
Пример:
alert( 0.1.toFixed(20) ); // 0.10000000000000000555
И когда мы суммируем 2 числа, их «неточности» тоже суммируются.
Вот почему 0.1 + 0.2 – это не совсем 0.3.Не только в JavaScript
Справедливости ради заметим, что ошибка в точности вычислений для чисел с плавающей точкой сохраняется в любом другом языке, где используется формат IEEE 754, включая PHP, Java, C, Perl, Ruby.
Можно ли обойти проблему? Конечно, наиболее надёжный способ — это округлить результат используя метод toFixed(n):
let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // 0.30
Помните, что метод toFixed всегда возвращает строку. Это гарантирует, что результат будет с заданным количеством цифр в десятичной части. Также это удобно для форматирования цен в интернет-магазине $0.30. В других случаях можно использовать унарный оператор +, чтобы преобразовать строку в число:
let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3
Также можно временно умножить число на 100 (или на большее), чтобы привести его к целому, выполнить математические действия, а после разделить обратно. Суммируя целые числа, мы уменьшаем погрешность, но она все равно появляется при финальном делении:
alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001
Таким образом, метод умножения/деления уменьшает погрешность, но полностью её не решает.
Иногда можно попробовать полностью отказаться от дробей. Например, если мы в нашем интернет-магазине начнём использовать центы вместо долларов. Но что будет, если мы применим скидку 30%? На практике у нас не получится полностью избавиться от дроби. Просто используйте округление, чтобы отрезать «хвосты», когда надо.Забавный пример
Попробуйте выполнить его:
// Привет! Я – число, растущее само по себе!
alert( 9999999999999999 ); // покажет 10000000000000000
Причина та же – потеря точности. Из 64 бит, отведённых на число, сами цифры числа занимают до 52 бит, остальные 11 бит хранят позицию десятичной точки и один бит – знак. Так что если 52 бит не хватает на цифры, то при записи пропадут младшие разряды.
Интерпретатор не выдаст ошибку, но в результате получится «не совсем то число», что мы и видим в примере выше. Как говорится: «как смог, так записал».Два нуля
Другим забавным следствием внутреннего представления чисел является наличие двух нулей: 0 и -0.
Все потому, что знак представлен отдельным битом, так что, любое число может быть положительным и отрицательным, включая нуль.
В большинстве случаев это поведение незаметно, так как операторы в JavaScript воспринимают их одинаковыми.