Автоматический выбор единиц

В экосистеме FormatJS работа с единицами измерения опирается на стандарты ECMAScript Internationalization API (ECMA-402), прежде всего на Intl.NumberFormat. Основная идея заключается в том, что единица измерения не является частью строки, а описывается через семантические параметры форматирования, что позволяет локали самостоятельно определять корректное представление.

FormatJS предоставляет надстройки над нативным API через пакеты react-intl, @formatjs/intl и polyfill-реализации, сохраняя совместимость с ICU MessageFormat.


Intl.NumberFormat и стиль unit

Базовый механизм работы с единицами реализуется через Intl.NumberFormat с опцией:

new Intl.NumberFormat('ru-RU', {
  style: 'unit',
  unit: 'kilometer'
}).format(5);

Ключевые элементы:

  • style: 'unit' — включает режим форматирования единиц
  • unit — тип измерения (kilometer, byte, second, liter и др.)
  • локаль (ru-RU, en-US) — определяет грамматику и формат вывода

Результат зависит от языка:

  • ru-RU5 км
  • en-US5 km

FormatJS использует этот же механизм внутри своих API, включая formatNumber.


Семантика единиц и локализация

Система единиц в CLDR (Unicode Common Locale Data Repository), используемая FormatJS, определяет:

  • сокращения (km, MB, h)
  • полные формы (kilometers, megabytes)
  • грамматические правила множественного числа
  • контекстные формы для разных локалей

Например:

new Intl.NumberFormat('en-GB', {
  style: 'unit',
  unit: 'liter',
  unitDisplay: 'long'
}).format(3);

Результат:

  • 3 litres

Параметр unitDisplay управляет формой:

  • short — компактная запись (3 l)
  • narrow — максимально короткая (3l)
  • long — полная форма (3 litres)

Автоматический выбор единиц как слой над Intl

Нативный API не выполняет автоматическое масштабирование величин. FormatJS также не включает встроенный универсальный конвертер, но предоставляет основу для построения логики автоматического выбора единиц на уровне приложения.

Автоматический выбор обычно строится вокруг следующих принципов:

  1. Нормализация входного значения
  2. Определение оптимального диапазона
  3. Выбор единицы измерения
  4. Передача результата в Intl.NumberFormat

Масштабирование величин и пороговые правила

Типичный подход — построение таблицы порогов:

Пример для байтов

const units = [
  { unit: 'byte', threshold: 1 },
  { unit: 'kilobyte', threshold: 1024 },
  { unit: 'megabyte', threshold: 1024 ** 2 },
  { unit: 'gigabyte', threshold: 1024 ** 3 }
];

Алгоритм выбора:

function formatBytes(value, locale) {
  let selected = units[0];

  for (const u of units) {
    if (value >= u.threshold) {
      selected = u;
    }
  }

  const scaled = value / selected.threshold;

  return new Intl.NumberFormat(locale, {
    style: 'unit',
    unit: selected.unit,
    maximumFractionDigits: 1
  }).format(scaled);
}

Поведение:

  • 512512 bytes
  • 20482 KB
  • 5_242_8805 MB

Автоматизация через адаптивные правила

Более сложные системы используют логарифмическое масштабирование:

  • вычисление степени 1024 или 1000
  • подбор единицы по индексу массива
  • округление с учетом локали
function formatAutoSize(value, locale) {
  const step = 1024;
  const labels = ['byte', 'kilobyte', 'megabyte', 'gigabyte'];

  const index = Math.floor(Math.log(value) / Math.log(step));
  const clamped = Math.max(0, Math.min(index, labels.length - 1));

  const scaled = value / Math.pow(step, clamped);

  return new Intl.NumberFormat(locale, {
    style: 'unit',
    unit: labels[clamped],
    maximumFractionDigits: 2
  }).format(scaled);
}

Применение в форматировании времени

Автоматический выбор единиц часто применяется к временным интервалам:

  • миллисекунды → секунды
  • секунды → минуты
  • минуты → часы
function formatDuration(seconds, locale) {
  const units = [
    { unit: 'second', limit: 60 },
    { unit: 'minute', limit: 3600 },
    { unit: 'hour', limit: Infinity }
  ];

  let value = seconds;
  let selected = units[0];

  for (const u of units) {
    if (seconds >= u.limit) selected = u;
  }

  if (selected.unit === 'minute') value = seconds / 60;
  if (selected.unit === 'hour') value = seconds / 3600;

  return new Intl.NumberFormat(locale, {
    style: 'unit',
    unit: selected.unit,
    maximumFractionDigits: 1
  }).format(value);
}

Integration с FormatJS (react-intl)

В react-intl форматирование выполняется через formatNumber:

import { useIntl } from 'react-intl';

function Component({ distance }) {
  const intl = useIntl();

  return intl.formatNumber(distance, {
    style: 'unit',
    unit: 'kilometer'
  });
}

Автоматический выбор единиц реализуется до вызова formatNumber, поскольку сам FormatJS не выполняет конвертацию значений.


Контекстная зависимость единиц

В сложных интерфейсах выбор единиц зависит от:

  • региональных настроек (metric vs imperial)
  • пользовательских предпочтений
  • доменной области (наука, финансы, UI метрики)
  • диапазона значений

Пример условной логики:

function formatDistance(valueMeters, locale, system = 'metric') {
  if (system === 'imperial') {
    const miles = valueMeters / 1609.34;

    return new Intl.NumberFormat(locale, {
      style: 'unit',
      unit: 'mile',
      maximumFractionDigits: 1
    }).format(miles);
  }

  const km = valueMeters / 1000;

  return new Intl.NumberFormat(locale, {
    style: 'unit',
    unit: 'kilometer',
    maximumFractionDigits: 1
  }).format(km);
}

Грамматическая адаптация и plural rules

FormatJS опирается на Intl.PluralRules, что влияет на:

  • выбор формы слова
  • согласование числительных
  • локализацию единиц

Пример:

  • 1 kilometer
  • 2 kilometers
  • 5 kilometers

В русском языке:

  • 1 километр
  • 2 километра
  • 5 километров

Это происходит автоматически при использовании style: 'unit'.


Ограничения и проектирование системы

Автоматический выбор единиц в FormatJS требует внешнего слоя логики:

  • отсутствует встроенный конвертер измерений
  • нет универсального “best unit picker”
  • ответственность за диапазоны лежит на приложении

Типовые архитектуры включают:

  • утилитарные функции (formatBytes, formatDuration)
  • конфигурационные таблицы единиц
  • контекст пользователя (locale + preferences)
  • кеширование выбранных форматов

Паттерны построения универсального форматтера

Распространённая структура:

function createUnitFormatter(locale, units) {
  return function(value) {
    const selected = selectUnit(value, units);
    const scaled = value / selected.factor;

    return new Intl.NumberFormat(locale, {
      style: 'unit',
      unit: selected.unit,
      maximumFractionDigits: 2
    }).format(scaled);
  };
}

Такой подход позволяет:

  • централизовать правила выбора
  • переиспользовать форматтер
  • поддерживать разные типы величин

Работа с нестандартными единицами

Некоторые значения не имеют прямого соответствия в CLDR:

  • токены
  • внутренние метрики системы
  • бизнес-единицы (покупки, очки, события)

В таких случаях применяется fallback:

new Intl.NumberFormat('en', {
  style: 'unit',
  unit: 'liter', // фиктивное соответствие
  unitDisplay: 'narrow'
}).format(value);

или отказ от unit:

new Intl.NumberFormat('en').format(value);

Оптимизация отображения больших диапазонов

При больших значениях применяется:

  • сокращение значимости цифр
  • ограничение дробной части
  • переключение единицы при превышении порога
maximumFractionDigits: 0
minimumFractionDigits: 0

или динамическое округление:

const precision = value > 1000 ? 0 : 2;

Интероперабельность с ICU MessageFormat

FormatJS поддерживает ICU-синтаксис:

import { defineMessage } from 'react-intl';

const message = defineMessage({
  defaultMessage: '{value} kilometers',
  description: 'Distance display'
});

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