Округление и точность

Работа с числами в интернационализированных приложениях в JavaScript почти всегда опирается на возможности стандарта ECMAScript Internationalization API (Intl). В экосистеме FormatJS эти механизмы дополняются ICU-совместимым синтаксисом сообщений, удобными обёртками и полифилами, которые позволяют контролировать точность отображения чисел, валют, процентов и временных интервалов.

Ключевая проблема форматирования заключается в том, что внутреннее представление чисел в JavaScript использует IEEE-754 floating point. Это приводит к накоплению погрешностей:

0.1 + 0.2 === 0.3 // false

Форматирование не должно отображать эти артефакты пользователю, поэтому используется округление и контроль точности на уровне форматтеров.


Базовые параметры точности в Intl.NumberFormat

Основной инструмент управления округлением — Intl.NumberFormat. В FormatJS он используется напрямую или через ICU-синтаксис сообщений.

Минимальная и максимальная точность

const nf = new Intl.NumberFormat("en-US", {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

nf.format(1.2);   // "1.20"
nf.format(1.236); // "1.24"

Смысл параметров:

  • minimumFractionDigits — гарантирует минимальное число знаков после запятой
  • maximumFractionDigits — ограничивает максимальную точность и инициирует округление

Механика округления

По умолчанию используется режим halfExpand (округление половин вверх). Однако современные реализации Intl позволяют управлять режимом явно:

const nf = new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 0,
  roundingMode: "floor"
});

nf.format(1.9); // "1"

Основные режимы округления

  • ceil — всегда вверх
  • floor — всегда вниз
  • expand — от нуля
  • trunc — усечение
  • halfCeil, halfFloor, halfExpand, halfTrunc, halfEven

Особое значение имеет halfEven (банковское округление), минимизирующее систематическую ошибку при больших выборках данных.


Точность и значащие цифры

Помимо дробной части, форматирование может контролироваться через значащие цифры:

const nf = new Intl.NumberFormat("en-US", {
  minimumSignificantDigits: 3,
  maximumSignificantDigits: 5
});

nf.format(1234.56); // "1,234.6"

Когда использовать значащие цифры

  • научные данные
  • измерения с переменной точностью
  • динамические диапазоны значений

ICU-синтаксис и FormatJS

В FormatJS используется ICU MessageFormat, который позволяет задавать числовые форматы прямо в строках сообщений:

const message = "Баланс: {value, number, ::currency/USD}";

или более явно:

const message = "{value, number, .##}";

Специальные паттерны

ICU поддерживает расширенные модификаторы:

  • ::currency/USD — валютное форматирование
  • ::percent — проценты
  • ::compact-short — компактное представление

Примеры:

"Доход: {income, number, ::currency/EUR}"
"Рост: {growth, number, ::percent}"
"Просмотры: {views, number, ::compact-short}"

Валюты и особенности округления

Валютные значения требуют строгого контроля дробной части, который задаётся спецификацией валюты:

const nf = new Intl.NumberFormat("ja-JP", {
  style: "currency",
  currency: "JPY"
});

nf.format(1234.56); // "¥1,235"

Особенности:

  • валюта определяет minor unit (например, JPY = 0 знаков)
  • округление происходит автоматически по правилам валюты
  • нельзя полагаться на входные данные без форматтера

Компактная запись и влияние точности

Компактный формат меняет представление числа, сохраняя точность округления:

const nf = new Intl.NumberFormat("en-US", {
  notation: "compact",
  maximumFractionDigits: 1
});

nf.format(1200);    // "1.2K"
nf.format(2500000); // "2.5M"

Здесь округление выполняется до уровня отображаемой значимости, а не исходного значения.


Проблемы двоичной арифметики и форматирование

JavaScript не гарантирует точное десятичное представление:

0.3 / 0.1 // 2.9999999999999996

Форматирование решает проблему только на уровне отображения, но не вычисления.

Типичная практика:

const value = Math.round((a + b) * 100) / 100;

Однако предпочтительнее полагаться на Intl:

new Intl.NumberFormat("en-US", {
  maximumFractionDigits: 2
}).format(a + b);

FormatJS и контроль точности в React Intl

В React-ориентированном использовании FormatJS форматирование часто происходит через компоненты:

import { FormattedNumber } from "react-intl";

<FormattedNumber
  value={1234.567}
  minimumFractionDigits={2}
  maximumFractionDigits={2}
/>

Или через ICU:

<FormattedMessage
  defaultMessage="Сумма: {amount, number, ::currency/USD}"
  values={{ amount: 1234.56 }}
/>

Здесь вся логика округления делегируется Intl, а не бизнес-коду.


Настройка полифилов FormatJS и влияние на точность

В экосистеме FormatJS часто используются полифилы:

  • @formatjs/intl-numberformat
  • @formatjs/intl-datetimeformat
  • @formatjs/intl-pluralrules

Они важны для:

  • старых браузеров
  • согласованного поведения между окружениями
  • поддержки новых режимов округления

Разные реализации могут по-разному трактовать edge cases округления, поэтому полифилы фиксируют поведение.


Локализация и различия в правилах округления

Разные локали влияют не только на разделители, но и на правила представления чисел:

  • в некоторых культурах используется группировка по 4 цифры
  • различается символ десятичного разделителя
  • могут отличаться правила compact notation

Пример:

new Intl.NumberFormat("de-DE").format(1234.56); // "1.234,56"
new Intl.NumberFormat("en-US").format(1234.56); // "1,234.56"

Округление остаётся одинаковым, но визуальное представление меняется.


Контроль погрешностей при бизнес-расчётах

Форматирование не заменяет арифметическую точность. В системах с деньгами или измерениями применяются дополнительные подходы:

  • хранение значений в минимальных единицах (центы)
  • использование decimal-библиотек (decimal.js, big.js)
  • разделение вычислений и отображения

FormatJS в этом контексте отвечает только за последний этап — представление.


Связь округления и шаблонов сообщений

ICU позволяет задавать формат прямо в контексте текста, что устраняет необходимость ручной обработки чисел:

"{count, plural,
  one {# элемент}
  other {# элементов}
}"

Хотя это не про округление напрямую, числовая точность влияет на выбор формы множественного числа при дробных значениях:

1.0 → one
1.2 → other

Поведение при отсутствующих параметрах точности

Если параметры не заданы:

  • используется локальный default
  • применяется roundingMode: "halfExpand"
  • число дробных знаков адаптивно
new Intl.NumberFormat("en-US").format(1.2);  // "1.2"
new Intl.NumberFormat("en-US").format(1.234); // "1.234"

Это поведение важно учитывать при построении UI, где требуется стабильный формат.


Композиция форматов и предсказуемость вывода

При комбинации параметров:

  • minimumFractionDigits
  • maximumFractionDigits
  • roundingMode
  • notation
  • compactDisplay

формируется итоговая стратегия округления, которая применяется до локализации.

Пример:

new Intl.NumberFormat("en-US", {
  notation: "compact",
  maximumFractionDigits: 2,
  roundingMode: "halfEven"
});

Результат зависит от всех параметров одновременно, а не от одного доминирующего.


Практика согласованного форматирования в FormatJS

В крупных приложениях FormatJS используется как слой унификации:

  • единый формат чисел в UI
  • консистентные правила округления
  • централизованные ICU-шаблоны
  • контроль локалей через provider

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