Округление и форматирование чисел в React: адаптивный подход
- вторник, 20 января 2026 г. в 00:00:05

Всем привет, на связи снова я — Дмитрий, React-разработчик. И сегодня хочу поднять тему, которая на первый взгляд кажется простой, но на практике может доставить немало сюрпризов - округление и форматирование чисел в интерфейсе.
Изначально может показаться, что здесь сложного практически ничего нет: есть toFixed() и toLocaleString(), но практика показывает, что реальные интерфейсы почти никогда в это не укладываются.
Почему? Потому что в разных диапазонах чисел пользователи ждут разного поведения.
Именно с этим сталкиваются разработчики при работе с таблицами, отчётами, финансовыми данными и аналитикой.
В этой статье я предложу функцию, которая решает определенные задачи округления и форматирования, и разберу, как она подбирает количество знаков после запятой в зависимости от величины числа
Ниже опишу требования к форматированию, которые легли в основу функции, и покажу, как они отражаются в коде и в интерфейсе.
Начнем с проверки входного значения. В самом начале нужно отбросить невалидные значения:
если значение равно null, undefined, NaN или Infinity, возвращается символ "–"
если значение строго равно 0, возвращается строка "0"
Намеренно разделяю проверки, потому что, во-первых, ноль и отсутствие данных — разные состояния, и пользователь должен их различать, во-вторых, технические значения вроде NaN или Infinity не должны попадать в UI — они не несут бизнес-смысла и только путают.
Если значение валидное, функция анализирует абсолютную величину числа, игнорируя знак. Знак важен для отображения, но не влияет на выбор точности.
Числа от 1 до 9 999 отображаются с двумя знаками после запятой, значение округляется до сотых, например 12.3456 → 12,35
Числа больше 10 000 отображаются без дробной части, число округляется до целого, например 123456.78 → 123 457
Для больших значений дробная часть, как правило, не несёт полезной информации, а только ухудшает читаемость.
Самая сложная и важная часть логики — работа с малыми значениями.
Для чисел меньше "1 функция":
определяет, сколько нулей идёт после запятой до первой значащей цифры
подбирает такое количество знаков после запятой, чтобы первая значащая цифра обязательно была видна
при этом гарантирует минимум 4 знака после запятой
С точки зрения пользователя это позволяет не превращать малые, но важные значения в 0, сохранить смысл числа, обеспечить визуальную стабильность таблиц и отчётов (именно здесь эта функция будет использоваться)
После того как количество знаков после запятой определено, число должно округлиться до нужной точности и, соответственно, если дробная часть не требуется, число округляется до целого. Также, т.к. мы живем в России, десятичный разделитель заменяется с точки на запятую.
Это делается на уровне форматирования, а не данных, чтобы не искажать бизнес-логику.
Целая часть числа форматируется с пробелами между тысячами: 1234567 → 1 234 567
Такой формат значительно повышает читаемость больших чисел и в целом широко распространен и удобен.
Итак, у меня получилась следующая функция, давайте посмотрим. Точнее две функции: одна для форматирования, другая - для округления. Полный ее код в конце.
Начнем с formatWithSpaces
const formatWithSpaces = (numStr: string): string => numStr.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');Здесь все просто – это регулярное выражение, которое ищет каждую позицию перед группой из трёх цифр и вставляет пробел.
Функция принимает число, которое нужно отобразить, да она поддерживает варианты number, null, undefined и их же проверят и возвращает отформатированную строку, с которой можно работать в UI.
Первые проверки занимаются именно этим – отсеивают невалидные значения.
if (value == null || value == undefined || !Number.isFinite(value)) return '–';
if (value === 0) return '0';А также вторая строка отдельно обрабатывает ноль, чтобы он всегда отображался как "0", а не пустая строка или "–". Это важно, чтобы пользователь видел разницу между нулём и отсутствием данных.
const calcFractionDigits = (val: number): number => {
const absVal = Math.abs(val);
if (absVal >= 1 && absVal < 10000) return 2;
if (absVal >= 10000) return 0;
const digits = Math.floor(-Math.log10(absVal));
return Math.max(digits + 1, 4);
};Разберём:
Сначала берём абсолютное значение, потому что знак числа не влияет на количество знаков после запятой. Дальше,если числа от 1 до 9 999, возвращаем 2 знака после запятой, а если 10 000 и больше, то по нашему тз отображаются без дробной части, соответственно возвращаем 0.
И вот тут самое интересное, как вычислить количество нулей после запятой у малых чисел.
Вот эта строчка:
const digits = Math.floor(-Math.log10(absVal));Math.log10(x) — это логарифм числа x по основанию 10, то есть отвечает на вопрос: 10 в какой степени даст x? Это то, что нам нужно:
x | log10(x) | Пояснение |
1 | 0 | 10^0 = 1 |
10 | 1 | 10^1 = 10 |
100 | 2 | 10^2 = 100 |
0.1 | -1 | 10^-1 = 0.1 |
0.01 | -2 | 10^-2 = 0.01 |
0.0012 | -2.9208… | 10^-2.9208 ≈ 0.0012 |
Т.к. для для чисел меньше 1 log10(absVal) будет отрицателен, например:
0.01 → log10(0.01) = -2
0.0012 → log10(0.0012) ≈ -2.9208
Соответственно, нужно взять отрицательное значение, чтобы получить положительное число, отражающее количество нулей после запятой до первой значащей цифры.
0.01 → -(-2) = 2
0.0012 → -(-2.9208) ≈ 2.9208
Дальше нам нужно округлить вниз до целого, для этого, логично, используем Math.floor, это нужно, чтобы определить количество «полных» нулей после запятой до первой значащей цифры.
Например: absVal = 0.12, тогда -log10(absVal) = 0.9208… и Math.floor(-log10(absVal)) = 0 – получается 0 нулей, первая цифра сразу после запятой.
Возьмем еще для примера absVal= 0.0012 его -log10(absVal) = 2.9208…, а Math.floor(-log10(absVal)) = 2. Итого получаем 2 нуля, первая значащая цифра на четвёртой позиции.
Т.е в общем и целом digits = количество нулей после запятой перед первой значащей цифрой.
А дальше это используется так, что после вычисления digits мы добавляем 1 и берём минимум 4 знака, чтобы определить количество знаков после запятой для toFixed.
В этой строчке
return Math.max(digits + 1, 4);Число | digits | digits + 1 | fractionDigits |
0.12 | 0 | 1 | 4 |
0.0012 | 2 | 3 | 4 |
0.0000123 | 4 | 5 | 5 |
Таким образом, первая значащая цифра никогда не теряется, а малые числа не превращаются в ноль.
const fractionDigits = calcFractionDigits(value);
const fixed = fractionDigits === 0
? Math.round(value).toString()
: value.toFixed(fractionDigits);
Вызываем calcFractionDigits и получаем количество знаков после запятой для данного числа. Если дробная часть не нужна (fractionDigits === 0), просто округляем Math.round. Иначе используем toFixed(fractionDigits), чтобы получить строку с нужной точностью.
const [intPart, fracPart] = fixed.split('.');Разделяем число на целую часть (intPart) и дробную часть (fracPart). Это нужно, чтобы отдельно отформатировать целую часть пробелами, а дробную оставить как есть.
const formattedInt = formatWithSpaces(intPart);Применяем функцию, которую рассмотрели в начале, чтобы разделить цифры тысяч пробелами регулярным выражением.
Осталось все собрать в финальную строку
return fracPart ? `${formattedInt},${fracPart}` : formattedInt;Здесь получается, если есть дробная часть — соединяем с целой через запятую, как принято в России. Если дробной части нет — возвращаем только целую часть. Таким образом, мы получаем читаемое, аккуратное и предсказуемое отображение числа для всех диапазонов.
const formatWithSpaces = (numStr: string): string => numStr.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
const formatAdaptiveNumber = (value?: number | null | undefined): string => {
if (value == null || value == undefined || !Number.isFinite(value)) return '–';
if (value === 0) return '0';
const calcFractionDigits = (val: number): number => {
const absVal = Math.abs(val);
if (absVal >= 1 && absVal < 10000) return 2;
if (absVal >= 10000) return 0;
const digits = Math.floor(-Math.log10(absVal));
return Math.max(digits + 1, 4);
};
const fractionDigits = calcFractionDigits(value);
const fixed = fractionDigits === 0
? Math.round(value).toString()
: value.toFixed(fractionDigits);
const [intPart, fracPart] = fixed.split('.');
const formattedInt = formatWithSpaces(intPart);
return fracPart ? `${formattedInt},${fracPart}` : formattedInt;
};
Я вывел в табличку результаты отработки функции, получилось так:

Функцию можно доработать для поддержки единиц измерения и масштабирования чисел, например, отображать числа в тысячах, миллионах или триллионах, если значения очень большие или автоматически подставлять суффикс и сокращать количество цифр для удобного восприятия. Оставим это на вдохновение, на потом.
В этой статье мы разобрали задачу, которая на первый взгляд кажется простой — округление и форматирование чисел, но на практике бывает сложной из-за определенных требований. Эта функция может быть полезна в таблицах, отчётах, финансовых и аналитических интерфейсах, где важна точность, аккуратность и предсказуемость отображения чисел. В итоге, правильное форматирование чисел — это не просто «красиво», это улучшение UX и снижение риска ошибок при восприятии данных. А на фронте мы этим и занимаемся =)