Округление чисел до двух знаков после запятой в JavaScript: как правильно?
Проблема с плавающей точкой и стандартными методами
Вы правы, 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" (гарантированно)
Библиотеки работают с числами как со строками, полностью избегая проблем с плавающей точкой. Для простых задач это может быть избыточно, но если точность критична — это единственный правильный путь.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги