Avatar ·

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

Всем привет! Я новичок в JavaScript и столкнулся с проблемой округления чисел. Мне нужно выводить цены в корзине интернет-магазина, и я хочу, чтобы они всегда отображались максимум с двумя знаками после запятой. Но если число целое (например, 10), то показывать просто "10", без десятичных знаков. Я пробовал использовать `toFixed(2)`, но он всегда добавляет нули: `(10).toFixed(2)` возвращает `"10.00"`, а мне нужно `"10"`. Пробовал `Math.round(num * 100) / 100` – это работает, но иногда даёт странные результаты из-за неточности чисел с плавающей точкой. Например, `Math.round(1.005 * 100) / 100` возвращает `1`, а не `1.01`, как я ожидал. Подскажите, как правильно округлить число до двух знаков после запятой, но при этом не показывать лишние нули, если они не нужны? Нужно надёжное решение, которое будет работать с любыми числами, включая отрицательные и очень маленькие значения (например, `0.0001` должно превратиться в `0`, а не в `0.00`). Примеры того, что я хочу получить: - `10` → `"10"` - `10.1` → `"10.1"` - `10.123` → `"10.12"` - `10.005` → `"10.01"` - `0.0001` → `"0"` Заранее спасибо за помощь! <<>>

Проблема с плавающей точкой и стандартными методами

Вы правы, toFixed(2) всегда добавляет нули, а Math.round(num * 100) / 100 страдает от неточностей двоичной арифметики. Например, 1.005 * 100 в JavaScript даёт 100.49999999999999, поэтому округление вниз даёт 1, а не 1.01. Это известный баг, связанный с IEEE 754.

Решение: комбинация Number.EPSILON и toFixed

Самый надёжный способ — использовать Number.EPSILON для компенсации погрешностей, а затем удалять лишние нули. Вот универсальная функция:

function roundToTwo(num) {
  // Добавляем минимальную погрешность для компенсации ошибок округления
  return +(Math.round((num + Number.EPSILON) * 100) / 100).toFixed(2);
}

function formatPrice(num) {
  const rounded = roundToTwo(num);
  // Убираем лишние нули, но оставляем максимум 2 знака
  return rounded.toFixed(2).replace(/\.?0+$/, '');
}

// Тесты
console.log(formatPrice(10));       // "10"
console.log(formatPrice(10.1));     // "10.1"
console.log(formatPrice(10.123));   // "10.12"
console.log(formatPrice(10.005));   // "10.01"
console.log(formatPrice(0.0001));   // "0"
console.log(formatPrice(-10.5));    // "-10.5"

Как это работает

  • Number.EPSILON — минимальная разница между 1 и следующим представимым числом. Добавление его перед умножением компенсирует ошибки типа 1.005 * 100.
  • Math.round(...) / 100 — стандартное округление до двух знаков.
  • .toFixed(2) — гарантирует, что у нас всегда 2 знака после запятой (для последующей обработки).
  • replace(/\.?0+$/, '') — удаляет точку и все нули в конце строки. Если после точки остаются значащие цифры (например, "10.10"), они сохранятся как "10.1".

Более универсальная версия (для любого количества знаков)

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

function formatDecimals(num, decimals = 2) {
  const rounded = roundToDecimals(num, decimals);
  return rounded.toFixed(decimals).replace(/\.?0+$/, '');
}

console.log(formatDecimals(1.2345, 3)); // "1.235"
console.log(formatDecimals(100, 0));    // "100"

Альтернатива: Intl.NumberFormat

Если вам нужно только отображение, а не математические операции, используйте Intl.NumberFormat:

function formatPriceLocale(num) {
  return new Intl.NumberFormat('ru-RU', {
    minimumFractionDigits: 0,
    maximumFractionDigits: 2
  }).format(num);
}

console.log(formatPriceLocale(10));     // "10"
console.log(formatPriceLocale(10.1));   // "10,1" (запятая для РФ)

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

Вывод

Для 99% случаев используйте первый вариант с Number.EPSILON и replace. Он корректен, быстр и легко читается. Если вам нужно только для отображения — Intl.NumberFormat будет ещё проще.

<<>>

Простой способ через умножение и toFixed без EPSILON

Если вам не нужна идеальная точность для критичных финансовых расчётов (например, для игр или нефинансовых приложений), можно использовать более короткий подход:

function formatToTwo(num) {
  return parseFloat(num.toFixed(2)).toString();
}

console.log(formatToTwo(10));       // "10"
console.log(formatToTwo(10.1));     // "10.1"
console.log(formatToTwo(10.123));   // "10.12"
console.log(formatToTwo(10.005));   // "10.01" (работает в большинстве браузеров)

Этот метод использует toFixed(2) для округления (он сам решает проблему с 1.005), а затем parseFloat убирает лишние нули. Минус — для очень малых чисел (0.0001) он вернёт "0", что вам и нужно, но для 0.0000001 может вернуть "1e-7" из-за экспоненциальной записи.

Безопасное решение для финансов (используйте библиотеку)

Для серьёзных финансовых расчётов (бухгалтерия, банки) не стоит писать свои велосипеды. Используйте библиотеку decimal.js или big.js:

// npm install decimal.js
const Decimal = require('decimal.js');

function formatDecimal(num) {
  return new Decimal(num).toFixed(2).replace(/\.?0+$/, '');
}

console.log(formatDecimal(10.005)); // "10.01" (гарантированно)

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