Магия чисел: почему 0.1 + 0.2 не равно 0.3?
Привет, будущий гуру JavaScript! Если ты когда-нибудь пытался сложить 0.1 и 0.2 в консоли браузера, то, скорее всего, испытал культурный шок:
console.log(0.1 + 0.2); // Выведет: 0.30000000000000004
Да-да, JavaScript не сошел с ума. Это стандартная проблема арифметики с плавающей точкой (IEEE 754). Компьютеры просто не могут идеально представить 0.1 в двоичной системе — это как пытаться записать 1/3 в десятичной. Получается бесконечная дробь.
И тут возникает классический вопрос с Stack Overflow, который набрал тысячи просмотров: «How to round to at most 2 decimal places, if necessary». Как вывести число красиво, без хвоста из «0000000004»? Как округлить до двух знаков, но только если это нужно?
В этой статье мы разберем пошаговый туториал, который решит проблему раз и навсегда. Ты узнаешь, почему toFixed() иногда врет, как работает Math.round() и какой метод выбрать для интернет-магазина, калькулятора или просто для красивого вывода цены.
Проблема: «Хочу 2 знака, но не больше!»
Представь, что ты пишешь корзину товаров. Цена товара — 10.00. Ты хочешь показать 10, а не 10.00. Но если цена 10.456, ты хочешь показать 10.46. Логика простая: округляем до двух знаков, но лишние нули отбрасываем.
Стандартный метод toFixed(2) делает так:
const price = 10;
console.log(price.toFixed(2)); // "10.00" — а нам нужно "10"
const price2 = 10.456;
console.log(price2.toFixed(2)); // "10.46" — тут ок
Проблема: toFixed() всегда возвращает строку и всегда добавляет нули. Это не подходит.
Второй нюанс — toFixed() может округлять странно из-за всё той же плавающей точки:
console.log((1.005).toFixed(2)); // "1.00" — а должно быть "1.01"!
Бинго! Вот почему вопрос на Stack Overflow так популярен. Давай разбираться с решениями.
Решение №1: Математическое округление (Math.round)
Самый надежный способ — это умножить число на 100, округлить, разделить обратно. Работает это так:
function roundToTwo(num) {
return Math.round(num * 100) / 100;
}
console.log(roundToTwo(10.456)); // 10.46
console.log(roundToTwo(10)); // 10
console.log(roundToTwo(1.005)); // 1 — упс, опять проблема
Видишь? 1.005 * 100 = 100.49999999999999 (из-за IEEE 754). Math.round округляет это вниз, получается 100, делим на 100 = 1. Нехорошо.
Вывод: Метод хорош для простых случаев, но не для финансовых расчетов. Для учебных примеров — пойдет.
Решение №2: Number.EPSILON — спаситель точности
Чтобы победить ошибку плавающей точки, можно добавить крошечную дельту — Number.EPSILON. Это минимальная разница между двумя числами, которую может различить JavaScript (примерно 2.2e-16).
function roundToTwo(num) {
return Math.round((num + Number.EPSILON) * 100) / 100;
}
console.log(roundToTwo(1.005)); // 1.01 — работает!
console.log(roundToTwo(10.456)); // 10.46
console.log(roundToTwo(10)); // 10
Почему это работает? Мы подталкиваем число чуть-чуть вверх, чтобы 100.4999... превратилось в 100.5... и округлилось правильно.
Важно: Этот метод не идеален для всех крайних случаев, но для 99% задач — отличное решение. Именно его часто советуют на Stack Overflow.
Решение №3: toFixed() + Number() — хитрый трюк
Еще один популярный способ — использовать toFixed(2), а потом преобразовать строку обратно в число через Number() или унарный плюс +. Лишние нули отвалятся сами:
function roundToTwo(num) {
return Number(num.toFixed(2));
}
console.log(roundToTwo(10.456)); // 10.46
console.log(roundToTwo(10)); // 10
console.log(roundToTwo(1.005)); // 1 — опять проблема!
Да, toFixed() сам по себе страдает от той же ошибки с 1.005. Но если твои числа не содержат таких «опасных» значений (а в реальных ценах это редкость), этот метод самый лаконичный.
Решение №4: toLocaleString — для красивого вывода
Если тебе нужно не просто округлить, а еще и отформатировать число для пользователя (с разделителями тысяч), используй toLocaleString:
function formatPrice(num) {
return num.toLocaleString('ru-RU', {
minimumFractionDigits: 0,
maximumFractionDigits: 2
});
}
console.log(formatPrice(1234.5)); // "1 234,5"
console.log(formatPrice(100)); // "100"
console.log(formatPrice(99.999)); // "100"
Этот метод не округляет в строгом смысле, он форматирует. И он идеально подходит для вывода цен в интерфейсе. maximumFractionDigits: 2 гарантирует, что дробных знаков будет не больше двух.
Решение №5: Универсальная функция (лучшая практика)
Давай соберем всё лучшее в одну функцию. Она будет округлять до любого количества знаков и обрабатывать крайние случаи:
/**
* Округляет число до указанного количества знаков после запятой.
* Убирает лишние нули.
*
* @param {number} num - исходное число
* @param {number} decimals - количество знаков (по умолчанию 2)
* @returns {number} округленное число
*/
function roundTo(num, decimals = 2) {
const factor = Math.pow(10, decimals);
return Math.round((num + Number.EPSILON) * factor) / factor;
}
// Тесты
console.log(roundTo(10.456, 2)); // 10.46
console.log(roundTo(10, 2)); // 10
console.log(roundTo(1.005, 2)); // 1.01
console.log(roundTo(5.999, 2)); // 6
console.log(roundTo(0.1 + 0.2, 2)); // 0.