Avatar ·

Округление до двух знаков после запятой в JavaScript: правильное решение

Всем привет! Столкнулся с, казалось бы, простой задачей: нужно округлять числа до двух знаков после запятой, но только если это необходимо. То есть если число 10.5 — оставить как есть, а 10.456 — округлить до 10.46. Проблема в том, что стандартные методы JavaScript ведут себя странно. Например, `toFixed(2)` возвращает строку, а не число, и при этом может давать неожиданные результаты из-за погрешностей чисел с плавающей точкой. ```javascript // Проблема с toFixed console.log(1.005.toFixed(2)); // Выводит "1.00" вместо ожидаемого "1.01" console.log(0.1 + 0.2); // 0.30000000000000004 ``` Пробовал использовать `Math.round(num * 100) / 100`, но это тоже даёт сбои: ```javascript // Проблема с Math.round console.log(Math.round(1.005 * 100) / 100); // 1 вместо 1.01 console.log(Math.round(1.255 * 100) / 100); // 1.25 вместо 1.26 ``` Подскажите, как правильно и надёжно округлять числа до двух знаков после запятой с учётом всех особенностей JavaScript? Нужно, чтобы результат был именно числом (number), а не строкой. <<>>

Правильное округление до двух знаков после запятой

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

Вот несколько надёжных решений, проверенных на практике:

Решение 1: Использование Number.EPSILON

Самый простой и популярный способ — добавить очень маленькое число (Number.EPSILON) перед округлением, чтобы компенсировать погрешности:

function roundToTwo(num) {
    return Math.round((num + Number.EPSILON) * 100) / 100;
}

// Примеры
console.log(roundToTwo(1.005));   // 1.01 ✓
console.log(roundToTwo(1.255));   // 1.26 ✓
console.log(roundToTwo(10.5));    // 10.5 ✓
console.log(roundToTwo(0.1 + 0.2)); // 0.3 ✓
console.log(roundToTwo(2.345));   // 2.35 ✓

Решение 2: Универсальная функция с любым количеством знаков

Если нужно округлять не только до двух знаков, вот более гибкий вариант:

function roundToDecimal(num, decimals = 2) {
    const multiplier = Math.pow(10, decimals);
    return Math.round((num + Number.EPSILON) * multiplier) / multiplier;
}

// Использование
console.log(roundToDecimal(1.005, 2));  // 1.01
console.log(roundToDecimal(3.14159, 3)); // 3.142
console.log(roundToDecimal(0.1 + 0.2, 2)); // 0.3

Решение 3: Без Number.EPSILON (через toFixed с преобразованием)

Можно использовать toFixed, но затем преобразовать результат обратно в число:

function roundToTwoFixed(num) {
    return Number(num.toFixed(2));
}

// Но помните про баг с 1.005
console.log(roundToTwoFixed(1.005)); // 1.00 — баг!
console.log(roundToTwoFixed(2.345)); // 2.35 — работает

Важно: toFixed сам по себе имеет баги с округлением (например, 1.005), поэтому первый способ с Number.EPSILON надёжнее.

Почему это работает?

  • Number.EPSILON — это наименьшая разница между двумя представимыми числами с плавающей точкой (примерно 2.22e-16).
  • Добавляя эту крошечную величину перед округлением, мы «подталкиваем» число вверх, компенсируя погрешности представления.
  • Например, 1.005 * 100 в JavaScript даёт 100.49999999999999, а не 100.5. Добавление Number.EPSILON исправляет это.

Рекомендации

  • Всегда используйте Number.EPSILON для надёжного округления.
  • Если работаете с деньгами, лучше перевести всё в копейки/центы (целые числа) и работать с ними.
  • Для критически точных расчётов (финансы, наука) используйте библиотеки вроде decimal.js или big.js.
<<>>

Альтернативный подход: через строковую обработку

Если вам не нравится идея с Number.EPSILON или вы хотите полный контроль над округлением, можно использовать строковый метод:

function roundToTwo(num) {
    // Преобразуем в строку с большим количеством знаков
    const str = num.toString();
    const decimalIndex = str.indexOf('.');
    
    // Если нет десятичной части или она короче 2 знаков — возвращаем как есть
    if (decimalIndex === -1 || str.length - decimalIndex - 1 <= 2) {
        return num;
    }
    
    // Округляем через toFixed и преобразуем обратно
    return Number(Number(num).toFixed(2));
}

Плюсы:

  • Понятная логика — вы сами решаете, когда округлять.
  • Можно легко адаптировать для разных правил округления (математическое, банковское).

Минусы:

  • Медленнее, чем математические методы.
  • Всё равно страдает от багов toFixed для некоторых чисел (например, 1.005).

Этот подход лучше использовать, когда вам нужно не просто округлить, а ещё и проверить, нужно ли это делать (например, для форматирования вывода). Для чисто математических операций всё же рекомендую первый вариант с Number.EPSILON.