Диапазоны чисел

Диапазоны чисел в локализации представляют собой отдельный класс задач, возникающих при отображении интервалов значений, где важно сохранить корректность форматирования для конкретной культуры: разделители тысяч, десятичные знаки, пробелы, символы валют, а также правила сокращения повторяющихся частей. В контексте Globalize, работающей поверх CLDR-данных, диапазоны чисел строятся не как примитивная конкатенация строк, а как результат применения локализованных правил форматирования отдельных чисел и правил их объединения в интервал.

CLDR (Common Locale Data Repository) определяет отдельные шаблоны для диапазонов, включая числовые, временные и валютные. Для чисел это выражается в виде правил, позволяющих корректно отображать диапазоны вроде «1–5», «1 000–2 000» или «1,5–3,75» в зависимости от локали. В некоторых языках используется короткое тире, в других — длинное тире с пробелами, а в третьих — полное повторение единиц измерения для устранения неоднозначности.

Globalize, опираясь на CLDR, разделяет задачу на два уровня: форматирование отдельных чисел и построение диапазонного представления. Первый уровень реализуется через Globalize.formatNumber, второй — через пользовательскую композицию или использование API, совместимого с Intl (если окружение его поддерживает), либо через шаблонные механизмы Globalize.


Любой диапазон в локализации начинается с корректного форматирования его границ. Globalize предоставляет API для форматирования чисел с учётом локали:

const Globalize = require("globalize");

Globalize.locale("de");

Globalize.formatNumber(1234567.89);
// "1.234.567,89"

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

Пример подготовки значений:

const start = Globalize.formatNumber(1000);
const end = Globalize.formatNumber(5000);

console.log(start + " – " + end);
// "1.000 – 5.000"

Здесь уже проявляется важный аспект: диапазон — это не просто две строки, а структурированная композиция, где каждый элемент уже локализован.


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

В разных языковых системах диапазоны воспринимаются неодинаково. Основные различия:

  • тип символа разделителя (–, —, -)
  • наличие или отсутствие пробелов вокруг разделителя
  • сокращение повторяющихся единиц измерения
  • особенности числовых группировок

CLDR описывает эти правила через шаблоны диапазонов, которые условно можно представить как:

{0}–{1}

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

Globalize не всегда напрямую экспонирует готовый formatRange для чисел, поэтому диапазон часто формируется вручную с учётом локализованных компонентов.


Построение числовых диапазонов через форматирование компонентов

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

function formatNumberRange(globalize, from, to) {
  const formattedFrom = globalize.formatNumber(from);
  const formattedTo = globalize.formatNumber(to);

  return `${formattedFrom} – ${formattedTo}`;
}

Globalize.locale("fr");

formatNumberRange(Globalize, 1200, 5600);
// "1 200 – 5 600"

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

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


Сокращение повторяющихся частей диапазона

CLDR допускает оптимизацию отображения диапазонов, при которой повторяющиеся элементы не дублируются.

Например:

  • Полный вариант: 1,000–1,999
  • Сокращённый вариант: 1,000–999

Такая форма возможна только при наличии контекста, в котором однозначность сохраняется.

Реализация подобного поведения требует анализа строкового представления чисел:

function compactRange(fromStr, toStr) {
  let i = 0;

  while (i < fromStr.length && i < toStr.length && fromStr[i] === toStr[i]) {
    i++;
  }

  const commonPrefix = fromStr.slice(0, i);
  const fromSuffix = fromStr.slice(i);
  const toSuffix = toStr.slice(i);

  return `${commonPrefix}${fromSuffix}–${toSuffix}`;
}

Применение этого подхода зависит от языка: в некоторых локалях сокращение недопустимо, поскольку нарушает читаемость чисел.


Диапазоны с десятичными значениями

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

Globalize.locale("en");

const a = Globalize.formatNumber(1.5);
const b = Globalize.formatNumber(3.75);

console.log(`${a} – ${b}`);
// "1.5 – 3.75"

Проблема возникает при разной точности:

const a = Globalize.formatNumber(1.5);
const b = Globalize.formatNumber(3.7);

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


Настройка точности чисел для диапазонов

Globalize позволяет управлять форматированием через параметры числа:

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

formatter(1.5);  // "1.50"
formatter(3.7);  // "3.70"

После этого диапазон становится структурно стабильным:

const from = formatter(1.5);
const to = formatter(3.7);

`${from} – ${to}`;
// "1.50 – 3.70"

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


Валютные диапазоны

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

  • стоять до числа (USD, GBP)
  • стоять после числа (EUR, RUB в некоторых локалях)
  • изменять позицию в зависимости от локали

Globalize предоставляет formatCurrency:

Globalize.locale("en");

Globalize.formatCurrency(100, "USD");
// "$100.00"

Для диапазонов валют применяется та же композиционная модель:

function formatCurrencyRange(globalize, from, to, currency) {
  return `${globalize.formatCurrency(from, currency)} – ${globalize.formatCurrency(to, currency)}`;
}

formatCurrencyRange(Globalize, 100, 500, "USD");
// "$100.00 – $500.00"

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


Влияние локали на символ диапазона

Символ, разделяющий границы диапазона, не является универсальным. В зависимости от локали используются:

  • короткое тире (–)
  • длинное тире (—)
  • дефис (-)
  • пробел + тире + пробел

CLDR хранит это как часть pattern-структуры диапазона.

Пример различий:

  • en: 1,000–2,000
  • fr: 1 000 – 2 000
  • de: 1.000–2.000

При отсутствии специализированного API разработчики часто вводят единый символ вручную, однако это приводит к потере локализационной точности.


Диапазоны как часть форматирования сообщений

Globalize поддерживает message-форматирование через CLDR plural rules и ICU-подобные шаблоны. Диапазоны могут быть встроены в сообщения:

const message = Globalize.messageFormatter("range-template");

message({ start: 10, end: 20 });

При этом шаблон может выглядеть так:

"{start} – {end}"

Более сложные сценарии допускают интеграцию с plural rules:

"{count, plural, one {{start}–{end} item} other {{start}–{end} items}}"

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


Нормализация входных данных для диапазонов

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

  • удалены лишние знаки
  • приведены к числовому типу
  • проверены на корректность порядка (from ≤ to)
  • согласованы по типу (integer/float/currency)
function normalizeRange(a, b) {
  const from = Math.min(a, b);
  const to = Math.max(a, b);

  return { from, to };
}

Это предотвращает неконсистентное отображение, например:

5000 – 1000

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


Производительность при массовом форматировании диапазонов

При обработке больших массивов данных (например, аналитические таблицы или финансовые отчёты) создание диапазонов может стать затратным из-за повторного вызова форматтеров.

Оптимизация заключается в переиспользовании экземпляров форматтеров:

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

function formatRangeFast(from, to) {
  return `${numberFormatter(from)} – ${numberFormatter(to)}`;
}

Снижение накладных расходов особенно заметно при рендеринге списков диапазонов в интерфейсах.


Согласование диапазонов с единицами измерения

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

10–20 км

или

10 km – 20 km

CLDR в некоторых локалях допускает сокращённую форму, где единица выводится один раз. Реализация этого поведения требует дополнительной логики:

function formatUnitRange(from, to, unit) {
  const f = Globalize.formatNumber(from);
  const t = Globalize.formatNumber(to);

  return `${f}–${t} ${unit}`;
}

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


Граничные случаи диапазонов

Особые случаи включают:

  • одинаковые значения (5–5)
  • отрицательные числа (-10–5)
  • нулевые диапазоны (0–0)
  • очень большие числа с экспоненциальной формой

Каждый из этих случаев требует согласованного форматирования через Globalize, чтобы избежать смешения форматов:

Globalize.formatNumber(-1000);
// "-1.000" (в зависимости от локали)

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


Структурная модель диапазона в приложениях

На уровне архитектуры диапазон чисел обычно представляется не строкой, а объектом:

{
  from: number,
  to: number,
  format: "number" | "currency" | "unit"
}

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

  • менять локаль без изменения данных
  • переключать форматирование (число / валюта)
  • адаптировать отображение под интерфейс

Globalize выступает в роли слоя преобразования, а не хранения данных.