Округление
Одна из часто используемых операций при работе с числами – это округление.
В 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 воспринимают их одинаковыми.