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

Разделите форматированные числа на компоненты, чтобы применить пользовательское оформление и создать сложные интерфейсы

Введение

Метод format() возвращает полностью отформатированную строку, например, "$1,234.56" или "1.5M". Это хорошо работает для простого отображения, но вы не можете стилизовать отдельные части по-разному. Вы не можете сделать символ валюты жирным, выделить десятичную часть другим цветом или применить пользовательскую разметку к определённым компонентам.

JavaScript предоставляет метод formatToParts() для решения этой проблемы. Вместо того чтобы возвращать одну строку, он возвращает массив объектов, каждый из которых представляет одну часть отформатированного числа. Каждая часть имеет тип, например, currency, integer или decimal, и значение, например, $, 1234 или .. Вы можете обработать эти части, чтобы применить пользовательское оформление, создать сложные макеты или интегрировать отформатированные числа в богатые пользовательские интерфейсы.

Почему сложно кастомизировать отформатированные строки

Когда вы получаете отформатированную строку, например, "$1,234.56", вы не можете легко определить, где заканчивается символ валюты и начинается число. Разные локали размещают символы в разных позициях. Некоторые локали используют разные разделители. Надёжный разбор таких строк требует сложной логики, которая дублирует правила форматирования, уже реализованные в API Intl.

Рассмотрим панель управления, которая отображает денежные суммы с символом валюты в другом цвете. С помощью format() вам нужно будет:

  1. Определить, какие символы являются символом валюты
  2. Учитывать пробелы между символом и числом
  3. Обрабатывать разные позиции символов в разных локалях
  4. Аккуратно разбирать строку, чтобы не нарушить число

Этот подход ненадёжен и подвержен ошибкам. Любое изменение правил форматирования локали нарушает вашу логику разбора.

Метод formatToParts() устраняет эту проблему, предоставляя компоненты отдельно. Вы получаете структурированные данные, которые точно указывают, какая часть за что отвечает, независимо от локали.

Использование formatToParts для получения компонентов числа

Метод formatToParts() работает идентично методу format(), за исключением возвращаемого значения. Вы создаёте форматтер с теми же параметрами, а затем вызываете formatToParts() вместо format().

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

const parts = formatter.formatToParts(1234.56);
console.log(parts);

Это выводит массив объектов:

[
  { type: "currency", value: "$" },
  { type: "integer", value: "1" },
  { type: "group", value: "," },
  { type: "integer", value: "234" },
  { type: "decimal", value: "." },
  { type: "fraction", value: "56" }
]

Каждый объект содержит свойство type, указывающее, что представляет часть, и свойство value, содержащее фактическую строку. Части появляются в том же порядке, что и в отформатированном выводе.

Вы можете проверить это, объединив все значения вместе:

const formatted = parts.map(part => part.value).join("");
console.log(formatted);
// Вывод: "$1,234.56"

Объединённые части дают точно такой же результат, как вызов format().

Понимание типов частей

Свойство type определяет каждый компонент. Различные параметры форматирования создают разные типы частей.

Для базового форматирования чисел:

const formatter = new Intl.NumberFormat("en-US");
const parts = formatter.formatToParts(1234.56);
console.log(parts);
// [
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

Тип integer представляет целую часть числа. Несколько частей integer появляются, когда разделители групп разбивают число. Тип group представляет разделитель тысяч. Тип decimal представляет десятичную точку. Тип fraction представляет цифры после десятичной точки.

Для форматирования валюты:

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

const parts = formatter.formatToParts(1234.56);
console.log(parts);
// [
//   { type: "currency", value: "€" },
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

Тип currency появляется перед или после числа в зависимости от локальных соглашений.

Для процентов:

const formatter = new Intl.NumberFormat("en-US", {
  style: "percent"
});

const parts = formatter.formatToParts(0.1234);
console.log(parts);
// [
//   { type: "integer", value: "12" },
//   { type: "percentSign", value: "%" }
// ]

Тип percentSign представляет символ процента.

Для компактной нотации:

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

const parts = formatter.formatToParts(1500000);
console.log(parts);
// [
//   { type: "integer", value: "1" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "5" },
//   { type: "compact", value: "M" }
// ]

Тип compact представляет индикатор масштаба, такой как K, M или B.

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

Основной случай использования formatToParts() — это применение различных стилей к разным компонентам. Вы можете обработать массив частей, чтобы обернуть определенные типы в HTML-элементы.

Выделение символа валюты жирным шрифтом:

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

const parts = formatter.formatToParts(1234.56);
const html = parts
  .map(part => {
    if (part.type === "currency") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Вывод: "<strong>$</strong>1,234.56"

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

Стилизация десятичных частей по-другому:

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

const parts = formatter.formatToParts(1234.5);
const html = parts
  .map(part => {
    if (part.type === "decimal" || part.type === "fraction") {
      return `<span class="text-gray-500">${part.value}</span>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Вывод: "1,234<span class="text-gray-500">.50</span>"

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

Цветовая кодировка отрицательных чисел

Финансовые приложения часто отображают отрицательные числа красным цветом. С помощью formatToParts() вы можете обнаружить знак минуса и применить соответствующее оформление.

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

function formatWithColor(number) {
  const parts = formatter.formatToParts(number);
  const hasMinusSign = parts.some(part => part.type === "minusSign");

  const html = parts
    .map(part => part.value)
    .join("");

  if (hasMinusSign) {
    return `<span class="text-red-600">${html}</span>`;
  }

  return html;
}

console.log(formatWithColor(-1234.56));
// Вывод: "<span class="text-red-600">-$1,234.56</span>"

console.log(formatWithColor(1234.56));
// Вывод: "$1,234.56"

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

Создание пользовательских числовых отображений с несколькими стилями

Сложные интерфейсы часто комбинируют несколько правил стилизации. Вы можете применять разные классы или элементы к различным типам частей одновременно.

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

function formatCurrency(number) {
  const parts = formatter.formatToParts(number);

  return parts
    .map(part => {
      switch (part.type) {
        case "currency":
          return `<span class="currency-symbol">${part.value}</span>`;
        case "integer":
          return `<span class="integer">${part.value}</span>`;
        case "group":
          return `<span class="group">${part.value}</span>`;
        case "decimal":
          return `<span class="decimal">${part.value}</span>`;
        case "fraction":
          return `<span class="fraction">${part.value}</span>`;
        case "minusSign":
          return `<span class="minus">${part.value}</span>`;
        default:
          return part.value;
      }
    })
    .join("");
}

console.log(formatCurrency(1234.56));
// Вывод: "<span class="currency-symbol">$</span><span class="integer">1</span><span class="group">,</span><span class="integer">234</span><span class="decimal">.</span><span class="fraction">56</span>"

Этот детализированный контроль позволяет точно стилизовать каждый компонент. Затем вы можете использовать CSS для стилизации каждого класса по-разному.

Все доступные типы частей

Свойство type может принимать следующие значения в зависимости от используемых параметров форматирования:

  • integer: Целые числа
  • fraction: Десятичные цифры
  • decimal: Десятичный разделитель
  • group: Разделитель тысяч
  • currency: Символ валюты
  • literal: Пробелы или другой текст, добавленный форматированием
  • percentSign: Символ процента
  • minusSign: Индикатор отрицательного числа
  • plusSign: Индикатор положительного числа (когда установлено signDisplay)
  • unit: Строка единицы измерения для форматирования единиц
  • compact: Индикатор масштаба в компактной нотации (K, M, B)
  • exponentInteger: Значение экспоненты в научной нотации
  • exponentMinusSign: Знак минус в экспоненте
  • exponentSeparator: Символ, разделяющий мантиссу и экспоненту
  • infinity: Представление бесконечности
  • nan: Представление "не число"
  • unknown: Неопознанные токены

Не каждая опция форматирования создает все типы частей. Полученные части зависят от числового значения и конфигурации форматтера.

Научная нотация создает части, связанные с экспонентой:

const formatter = new Intl.NumberFormat("en-US", {
  notation: "scientific"
});

const parts = formatter.formatToParts(1234);
console.log(parts);
// [
//   { type: "integer", value: "1" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "234" },
//   { type: "exponentSeparator", value: "E" },
//   { type: "exponentInteger", value: "3" }
// ]

Специальные значения создают определенные типы частей:

const formatter = new Intl.NumberFormat("en-US");

console.log(formatter.formatToParts(Infinity));
// [{ type: "infinity", value: "∞" }]

console.log(formatter.formatToParts(NaN));
// [{ type: "nan", value: "NaN" }]

Создание доступных числовых отображений

Вы можете использовать formatToParts() для добавления атрибутов доступности к форматированным числам. Это помогает экранным чтецам правильно озвучивать значения.

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

function formatAccessibleCurrency(number) {
  const parts = formatter.formatToParts(number);
  const formatted = parts.map(part => part.value).join("");

  return `<span aria-label="${number} долларов США">${formatted}</span>`;
}

console.log(formatAccessibleCurrency(1234.56));
// Вывод: "<span aria-label="1234.56 долларов США">$1,234.56</span>"

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

Выделение определенных диапазонов чисел

Некоторые приложения выделяют числа, которые попадают в определенные диапазоны. С помощью formatToParts() вы можете применять стили в зависимости от значения, сохраняя при этом правильное форматирование.

const formatter = new Intl.NumberFormat("en-US");

function formatWithThreshold(number, threshold) {
  const parts = formatter.formatToParts(number);
  const formatted = parts.map(part => part.value).join("");

  if (number >= threshold) {
    return `<span class="text-green-600 font-bold">${formatted}</span>`;
  }

  return formatted;
}

console.log(formatWithThreshold(1500, 1000));
// Вывод: "<span class="text-green-600 font-bold">1,500</span>"

console.log(formatWithThreshold(500, 1000));
// Вывод: "500"

Число получает правильное форматирование для локали, а условное стилизование применяется на основе бизнес-логики.

Когда использовать formatToParts вместо format

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

Используйте formatToParts(), когда вам нужно:

  • Применить разное стилизование к разным частям числа
  • Создавать HTML или JSX с отформатированными числами
  • Добавлять атрибуты или метаданные к определенным компонентам
  • Интегрировать отформатированные числа в сложные макеты
  • Программно обрабатывать отформатированный вывод

Метод formatToParts() имеет немного большую нагрузку, чем format(), так как он создает массив объектов вместо одной строки. Эта разница незначительна для типичных приложений, но если вы форматируете тысячи чисел в секунду, format() работает быстрее.

Для большинства приложений выбирайте метод в зависимости от ваших потребностей в стилизовании, а не от производительности. Если вам не нужно настраивать вывод, используйте format(). Если вам нужно пользовательское стилизование или разметка, используйте formatToParts().

Как части сохраняют форматирование, специфичное для локали

Массив parts автоматически поддерживает правила форматирования, специфичные для локали. Разные локали размещают символы в разных позициях и используют разные разделители, но formatToParts() обрабатывает эти различия.

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

console.log(usdFormatter.formatToParts(1234.56));
// [
//   { type: "currency", value: "$" },
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

const eurFormatter = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR"
});

console.log(eurFormatter.formatToParts(1234.56));
// [
//   { type: "integer", value: "1" },
//   { type: "group", value: "." },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "," },
//   { type: "fraction", value: "56" },
//   { type: "literal", value: " " },
//   { type: "currency", value: "€" }
// ]

Немецкое форматирование размещает валюту после числа с пробелом. Разделитель групп — точка, а десятичный разделитель — запятая. Ваш код стилизации обрабатывает массив parts одинаково независимо от локали, а форматирование адаптируется автоматически.

Тип literal представляет любой пробел или текст, добавленный форматировщиком, который не подходит под другие категории. В немецком форматировании валюты он представляет пробел между числом и символом валюты.

Комбинирование formatToParts с компонентами фреймворков

Современные фреймворки, такие как React, могут использовать formatToParts() для эффективного создания компонентов.

function CurrencyDisplay({ value, locale, currency }) {
  const formatter = new Intl.NumberFormat(locale, {
    style: "currency",
    currency: currency
  });

  const parts = formatter.formatToParts(value);

  return (
    <span className="currency-display">
      {parts.map((part, index) => {
        if (part.type === "currency") {
          return <strong key={index}>{part.value}</strong>;
        }
        if (part.type === "fraction" || part.type === "decimal") {
          return <span key={index} className="text-sm text-gray-500">{part.value}</span>;
        }
        return <span key={index}>{part.value}</span>;
      })}
    </span>
  );
}

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