Округление валютных значений

При работе с денежными значениями проблема округления возникает практически в каждой финансовой системе. Разные валюты используют различные правила отображения дробной части:

  • доллар США — 2 знака после запятой;
  • японская иена — без дробной части;
  • бахрейнский динар — 3 знака;
  • некоторые криптовалюты — до 8 и более знаков.

Библиотека Globalize использует данные CLDR и позволяет автоматически применять корректные правила округления для конкретной локали и валюты.


Форматирование валют через Globalize

Базовое форматирование валют выполняется через метод currencyFormatter().

const formatter = Globalize.currencyFormatter("USD");

formatter(10.235);
// "$10.24"

Значение 10.235 автоматически округляется до двух знаков после запятой, поскольку для доллара США стандартом является точность до центов.


Подключение CLDR-данных

Для корректной работы валютного форматирования необходимо загрузить данные CLDR.

const Globalize = require("globalize");

Globalize.load(
  require("cldr-data/main/en/numbers"),
  require("cldr-data/main/en/currencies"),
  require("cldr-data/supplemental/currencyData"),
  require("cldr-data/supplemental/likelySubtags")
);

Globalize.locale("en");

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


Автоматическое количество дробных знаков

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

const usd = Globalize.currencyFormatter("USD");
const jpy = Globalize.currencyFormatter("JPY");
const bhd = Globalize.currencyFormatter("BHD");

console.log(usd(100.555));
// $100.56

console.log(jpy(100.555));
// ¥101

console.log(bhd(100.555));
// BHD 100.555

Globalize использует информацию из CLDR:

Валюта Количество знаков
USD 2
JPY 0
BHD 3

Принцип округления

Globalize использует стандартное математическое округление.

Примеры

const formatter = Globalize.currencyFormatter("USD");

console.log(formatter(10.234));
// $10.23

console.log(formatter(10.235));
// $10.24

console.log(formatter(10.236));
// $10.24

Логика:

  • числа меньше 5 округляются вниз;
  • числа 5 и больше округляются вверх.

Работа с отрицательными значениями

Округление применяется одинаково и для отрицательных чисел.

const formatter = Globalize.currencyFormatter("USD");

console.log(formatter(-10.235));
// -$10.24

Округление очень больших чисел

Globalize корректно работает с крупными денежными значениями.

const formatter = Globalize.currencyFormatter("EUR");

console.log(formatter(987654321.987));
// €987,654,321.99

При этом:

  • применяется разделение разрядов;
  • сохраняются валютные правила;
  • используется локализованное представление числа.

Влияние локали на отображение

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

Globalize.locale("en");

const enFormatter =
  Globalize.currencyFormatter("EUR");

console.log(enFormatter(10.235));
// €10.24
Globalize.locale("de");

const deFormatter =
  Globalize.currencyFormatter("EUR");

console.log(deFormatter(10.235));
// 10,24 €

Изменяются:

  • разделители дробной части;
  • позиция символа валюты;
  • формат группировки разрядов.

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


Принудительное изменение точности

По умолчанию Globalize использует стандарт валюты, однако точность можно переопределить вручную.

const formatter = Globalize.numberFormatter({
  style: "currency",
  currency: "USD",
  minimumFractionDigits: 4,
  maximumFractionDigits: 4
});

console.log(formatter(10.23567));
// $10.2357

Здесь:

  • число округляется до 4 знаков;
  • стандарт USD игнорируется;
  • применяется пользовательская точность.

minimumFractionDigits и maximumFractionDigits

Эти параметры особенно важны для финансовых систем.

minimumFractionDigits

Определяет минимальное количество знаков после запятой.

const formatter = Globalize.numberFormatter({
  style: "currency",
  currency: "USD",
  minimumFractionDigits: 2
});

console.log(formatter(10));
// $10.00

maximumFractionDigits

Определяет максимальное количество знаков.

const formatter = Globalize.numberFormatter({
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 1
});

console.log(formatter(10.28));
// $10.3

Жёсткое округление до целых

Иногда требуется полностью убрать дробную часть.

const formatter = Globalize.numberFormatter({
  style: "currency",
  currency: "USD",
  maximumFractionDigits: 0
});

console.log(formatter(10.9));
// $11

Банковское округление

Globalize не реализует банковское округление (banker's rounding) напрямую.

Стандартное округление:

1.5 -> 2
2.5 -> 3
3.5 -> 4

Банковское округление:

1.5 -> 2
2.5 -> 2
3.5 -> 4

Для таких сценариев округление обычно выполняется отдельно, а затем результат передаётся в Globalize для форматирования.


Проблемы JavaScript при округлении

Следует учитывать ограничения чисел с плавающей точкой в JavaScript.

Пример:

console.log(0.1 + 0.2);
// 0.30000000000000004

Это влияет и на валютные вычисления.

const formatter =
  Globalize.currencyFormatter("USD");

console.log(formatter(0.1 + 0.2));
// $0.30

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


Использование Decimal-библиотек

В серьёзных финансовых приложениях часто используют:

  • decimal.js
  • big.js
  • bignumber.js

Пример совместного использования:

const Decimal = require("decimal.js");

const value =
  new Decimal("10.235")
    .toDecimalPlaces(2);

const formatter =
  Globalize.currencyFormatter("USD");

console.log(formatter(value.toNumber()));
// $10.24

Такой подход:

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

Округление при конвертации валют

После конвертации часто появляются длинные дробные значения.

const usdToEur = 0.923451;

const result = 100 * usdToEur;

console.log(result);
// 92.3451

Globalize автоматически округлит значение согласно валюте.

const formatter =
  Globalize.currencyFormatter("EUR");

console.log(formatter(result));
// €92.35

Округление налогов и комиссий

Финансовые операции нередко требуют разных правил округления на разных этапах.

Пример:

const subtotal = 10.235;
const tax = subtotal * 0.2;

const formatter =
  Globalize.currencyFormatter("USD");

console.log(formatter(tax));
// $2.05

Важно понимать разницу между:

  • округлением промежуточных значений;
  • округлением итоговой суммы;
  • округлением каждой позиции отдельно.

Даже небольшие различия могут приводить к расхождению итогов.


Кэширование форматтеров

Создание форматтера — относительно дорогая операция.

Неправильно:

function formatPrice(value) {
  return Globalize
    .currencyFormatter("USD")(value);
}

Правильно:

const usdFormatter =
  Globalize.currencyFormatter("USD");

function formatPrice(value) {
  return usdFormatter(value);
}

Это особенно важно:

  • при обработке больших таблиц;
  • в интернет-магазинах;
  • в финансовых панелях;
  • в аналитических интерфейсах.

Форматирование без символа валюты

Иногда требуется только округление, без отображения денежного символа.

const formatter = Globalize.numberFormatter({
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
});

console.log(formatter(10.235));
// 10.24

Сравнение с Intl.NumberFormat

Современный JavaScript предоставляет встроенный API:

const formatter =
  new Intl.NumberFormat("en", {
    style: "currency",
    currency: "USD"
  });

console.log(formatter.format(10.235));
// $10.24

Однако Globalize предоставляет:

  • более гибкую интеграцию с CLDR;
  • расширяемую архитектуру;
  • единый подход к локализации;
  • поддержку дополнительных модулей интернационализации.

Обработка NaN и некорректных значений

Globalize ожидает корректные числовые значения.

const formatter =
  Globalize.currencyFormatter("USD");

console.log(formatter(NaN));
// NaN

Перед форматированием обычно выполняется валидация:

function safeFormat(value) {
  if (typeof value !== "number") {
    return "";
  }

  if (Number.isNaN(value)) {
    return "";
  }

  return usdFormatter(value);
}

Округление в пользовательских функциях

Часто создают обёртки над Globalize.

function createCurrencyFormatter(currency) {
  const formatter =
    Globalize.currencyFormatter(currency);

  return value => formatter(value);
}

Использование:

const formatUsd =
  createCurrencyFormatter("USD");

console.log(formatUsd(19.999));
// $20.00

Точность и хранение денежных значений

Распространённая практика — хранить деньги в минимальных единицах.

Пример:

const cents = 1099;

const dollars = cents / 100;

const formatter =
  Globalize.currencyFormatter("USD");

console.log(formatter(dollars));
// $10.99

Преимущества:

  • отсутствие ошибок плавающей точки;
  • предсказуемые вычисления;
  • удобство финансовой отчётности.

Практический пример интернет-магазина

const priceFormatter =
  Globalize.currencyFormatter("USD");

const products = [
  { name: "Keyboard", price: 99.995 },
  { name: "Mouse", price: 25.499 },
  { name: "Monitor", price: 199.991 }
];

products.forEach(product => {
  console.log(
    `${product.name}: ${
      priceFormatter(product.price)
    }`
  );
});

Результат:

Keyboard: $100.00
Mouse: $25.50
Monitor: $199.99

Практический пример мультивалютной системы

const formatters = {
  USD: Globalize.currencyFormatter("USD"),
  EUR: Globalize.currencyFormatter("EUR"),
  JPY: Globalize.currencyFormatter("JPY")
};

function formatMoney(value, currency) {
  return formatters[currency](value);
}

console.log(formatMoney(10.235, "USD"));
// $10.24

console.log(formatMoney(10.235, "EUR"));
// €10.24

console.log(formatMoney(10.235, "JPY"));
// ¥10

Globalize автоматически применяет нужные правила округления для каждой валюты.