Как форматировать диапазоны, например 3–5 или 100–200
Используйте JavaScript для отображения числовых диапазонов с учетом локальных стандартов форматирования
Введение
Числовые диапазоны часто встречаются в пользовательских интерфейсах. Диапазоны цен отображаются как $100–$200, номера страниц — как 1–10, а примерные количества — как 3–5 штук. Такие диапазоны показывают, что значение находится между двумя границами, предоставляя пользователю ориентировочную информацию, а не точное число.
Если вы жёстко задаёте разделитель между значениями диапазона, вы предполагаете, что все пользователи придерживаются одинаковых типографских стандартов. В английском обычно используют дефис или среднее тире, но в других языках применяются разные символы или даже слова. Например, в немецком между числами ставят «bis», а в некоторых языках вокруг разделителя добавляют пробелы.
В JavaScript есть метод formatRange() у Intl.NumberFormat, который автоматически форматирует диапазоны с учётом локали. Этот метод применяет локальные правила как к числам, так и к разделителю, чтобы диапазоны отображались корректно для пользователей по всему миру.
Почему числовые диапазоны требуют локального форматирования
В разных культурах сложились свои правила для записи диапазонов. Они касаются как символа-разделителя, так и пробелов вокруг него.
В американском английском для диапазонов обычно используют среднее тире без пробелов: 3–5, 100–200. В некоторых стилях допускаются пробелы вокруг тире: 3 – 5. Конкретные правила зависят от контекста и стандартов публикации.
В немецком диапазоны часто разделяют словом «bis»: 3 bis 5, 100 bis 200. Такой подход делает связь между числами явной, а не зависящей только от знаков препинания.
В испанском диапазоны могут оформляться как с помощью тире, так и с помощью слова «a»: 3–5 или 3 a 5. Выбор зависит от региона и контекста.
Когда вы форматируете диапазоны, включающие валюту или единицы измерения, задача усложняется. Например, ценовой диапазон в американском английском выглядит как $100-$200, а в немецком — как 100 €-200 €, либо 100-200 € с символом только один раз. В разных локалях символы валюты располагаются по-разному и повторяются по-разному.
При ручном форматировании диапазонов нужно знать эти правила и реализовывать логику для каждой локали отдельно. API Intl уже содержит эти знания и применяет нужное форматирование в зависимости от выбранной локали.
Использование formatRange для форматирования числовых диапазонов
Метод formatRange() принимает два числа и возвращает строку с отформатированным диапазоном. Создайте экземпляр Intl.NumberFormat с нужной локалью и опциями, затем вызовите formatRange() с начальным и конечным значениями.
const formatter = new Intl.NumberFormat("en-US");
console.log(formatter.formatRange(3, 5));
// Output: "3–5"
console.log(formatter.formatRange(100, 200));
// Output: "100–200"
console.log(formatter.formatRange(1000, 5000));
// Output: "1,000–5,000"
Форматтер добавляет разделители тысяч к обоим числам в диапазоне и использует подходящий разделитель между ними. Для американского английского это длинное тире без пробелов.
Вы можете отформатировать один и тот же диапазон для разных локалей, просто изменив идентификатор локали.
const usFormatter = new Intl.NumberFormat("en-US");
console.log(usFormatter.formatRange(100, 200));
// Output: "100–200"
const deFormatter = new Intl.NumberFormat("de-DE");
console.log(deFormatter.formatRange(100, 200));
// Output: "100–200"
const esFormatter = new Intl.NumberFormat("es-ES");
console.log(esFormatter.formatRange(100, 200));
// Output: "100-200"
В каждой локали свои правила для разделителей и пробелов. API автоматически учитывает эти детали в соответствии с типографическими стандартами локали.
Форматирование диапазонов валют
Форматирование диапазонов работает с любыми опциями форматирования чисел, включая валюту. При форматировании валютных диапазонов форматтер сам расставляет символы валюты и разделители диапазона.
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0
});
console.log(formatter.formatRange(100, 200));
// Output: "$100 – $200"
console.log(formatter.formatRange(1000, 5000));
// Output: "$1,000 – $5,000"
Форматтер ставит символ валюты перед каждым числом в диапазоне. Это ясно показывает, что оба значения — это суммы в валюте.
В разных локалях символы валюты располагаются по-разному.
const usFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0
});
console.log(usFormatter.formatRange(100, 200));
// Output: "$100 – $200"
const deFormatter = new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
maximumFractionDigits: 0
});
console.log(deFormatter.formatRange(100, 200));
// Output: "100–200 €"
В немецком форматировании символ евро ставится после диапазона, а не перед каждым числом. Это соответствует немецким типографским правилам оформления денежных диапазонов.
Что происходит, если значения диапазона примерно равны
Если после форматирования начальное и конечное значения округляются к одному и тому же числу, форматтер объединяет диапазон и может добавить символ приближения.
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0
});
console.log(formatter.formatRange(100, 200));
// Output: "$100 – $200"
console.log(formatter.formatRange(100, 120));
// Output: "$100 – $120"
console.log(formatter.formatRange(100.2, 100.8));
// Output: "~$100"
В третьем примере показаны два значения, которые округляются к одному целому числу. Вместо отображения "$100 – $100", что не несёт информации о диапазоне, форматтер выводит "~$100". Тильда указывает, что значение приблизительное.
Такое поведение применяется, если параметры форматирования приводят к одинаковому отображению начального и конечного значений.
const formatter = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 1
});
console.log(formatter.formatRange(2.9, 3.1));
// Output: "~3"
console.log(formatter.formatRange(2.94, 2.96));
// Output: "~2.9"
Форматтер добавляет символ приближения только при необходимости. Если значения округляются к разным числам, они отображаются как обычный диапазон.
Форматирование диапазонов с десятичными знаками
При форматировании диапазона сохраняются настройки количества знаков после запятой из параметров форматтера. Можно управлять точностью для обоих значений в диапазоне.
const formatter = new Intl.NumberFormat("en-US", {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
console.log(formatter.formatRange(3.5, 5.7));
// Output: "3.50–5.70"
console.log(formatter.formatRange(100, 200));
// Output: "100.00–200.00"
Форматтер применяет настройки десятичных знаков к обоим числам в диапазоне. Это обеспечивает единообразную точность по всему диапазону.
Десятичное форматирование можно комбинировать с валютой или другими стилями.
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
console.log(formatter.formatRange(99.99, 199.99));
// Output: "$99.99 – $199.99"
Форматирование числовых диапазонов на разных языках
Форматирование диапазонов подстраивается под правила оформления чисел, разделителей и пробелов в каждом языке.
const enFormatter = new Intl.NumberFormat("en-US");
console.log(enFormatter.formatRange(1000, 5000));
// Output: "1,000–5,000"
const deFormatter = new Intl.NumberFormat("de-DE");
console.log(deFormatter.formatRange(1000, 5000));
// Output: "1.000–5.000"
const frFormatter = new Intl.NumberFormat("fr-FR");
console.log(frFormatter.formatRange(1000, 5000));
// Output: "1 000–5 000"
const jaFormatter = new Intl.NumberFormat("ja-JP");
console.log(jaFormatter.formatRange(1000, 5000));
// Output: "1,000~5,000"
В английском для разделения тысяч используются запятые, а для диапазона — длинное тире. В немецком — точки для тысяч и длинное тире. Во французском — пробелы для тысяч и длинное тире. В японском — запятые для тысяч и тильда (~) для диапазона.
Эти различия распространяются и на форматирование валюты.
const enFormatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD"
});
console.log(enFormatter.formatRange(100, 200));
// Output: "$100.00 – $200.00"
const deFormatter = new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR"
});
console.log(deFormatter.formatRange(100, 200));
// Output: "100,00–200,00 €"
const jaFormatter = new Intl.NumberFormat("ja-JP", {
style: "currency",
currency: "JPY"
});
console.log(jaFormatter.formatRange(100, 200));
// Output: "¥100~¥200"
В каждом языке свои правила размещения символа валюты, разделителей десятичных и диапазонов. API автоматически учитывает все эти различия.
Совмещение formatRange с компактным отображением чисел
Форматирование диапазона работает с компактной записью, позволяя отображать диапазоны вроде 1K–5K или 1M–5M.
const formatter = new Intl.NumberFormat("en-US", {
notation: "compact"
});
console.log(formatter.formatRange(1000, 5000));
// Output: "1K–5K"
console.log(formatter.formatRange(1000000, 5000000));
// Output: "1M–5M"
console.log(formatter.formatRange(1200, 4800));
// Output: "1.2K–4.8K"
Форматер применяет компактную запись к обоим значениям в диапазоне. Это делает результат лаконичным, но информативным.
Если диапазон охватывает разные порядки величин, форматер корректно обрабатывает каждое значение.
const formatter = new Intl.NumberFormat("en-US", {
notation: "compact"
});
console.log(formatter.formatRange(500, 1500));
// Output: "500–1.5K"
console.log(formatter.formatRange(900000, 1200000));
// Output: "900K–1.2M"
Начальное значение может быть без компактной записи, а конечное — с ней, или наоборот, с разными обозначениями порядка. Форматер выбирает подходящий вариант в зависимости от размера каждого значения.
Использование formatRangeToParts для кастомного оформления
Метод formatRangeToParts() возвращает массив объектов, представляющих части форматированного диапазона. Это позволяет стилизовать или изменять отдельные компоненты диапазона.
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0
});
const parts = formatter.formatRangeToParts(100, 200);
console.log(parts);
Результат — массив объектов, каждый из которых содержит свойства type, value и source.
[
{ type: "currency", value: "$", source: "startRange" },
{ type: "integer", value: "100", source: "startRange" },
{ type: "literal", value: " – ", source: "shared" },
{ type: "currency", value: "$", source: "endRange" },
{ type: "integer", value: "200", source: "endRange" }
]
Свойство type определяет, что именно представляет часть: символ валюты, целое число, разделитель или текст. Свойство value содержит отформатированный текст. Свойство source указывает, относится ли часть к началу, концу диапазона или общая для обоих.
Эти части можно использовать для создания кастомного HTML с разным оформлением для разных компонентов.
const formatter = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0
});
const parts = formatter.formatRangeToParts(100, 200);
let html = "";
parts.forEach(part => {
if (part.type === "currency") {
html += `<span class="currency-symbol">${part.value}</span>`;
} else if (part.type === "integer") {
html += `<span class="amount">${part.value}</span>`;
} else if (part.type === "literal") {
html += `<span class="separator">${part.value}</span>`;
} else {
html += part.value;
}
});
console.log(html);
// Output: <span class="currency-symbol">$</span><span class="amount">100</span><span class="separator"> – </span><span class="currency-symbol">$</span><span class="amount">200</span>
Этот подход позволяет применять CSS-классы, добавлять подсказки или реализовывать другие кастомные функции, сохраняя корректное форматирование для выбранной локали.
Обработка крайних случаев с formatRange
Метод formatRange() включает обработку ошибок для некорректных входных данных. Если любой из параметров равен undefined, будет выброшено исключение TypeError. Если любой из параметров равен NaN или не может быть преобразован в число, будет выброшено исключение RangeError.
const formatter = new Intl.NumberFormat("en-US");
try {
console.log(formatter.formatRange(100, undefined));
} catch (error) {
console.log(error.name);
// Output: "TypeError"
}
try {
console.log(formatter.formatRange(NaN, 200));
} catch (error) {
console.log(error.name);
// Output: "RangeError"
}
При работе с пользовательским вводом или данными из внешних источников проверяйте, что значения — это корректные числа, прежде чем передавать их в formatRange().
Метод принимает числа, значения BigInt или строки, которые представляют корректные числа.
const formatter = new Intl.NumberFormat("en-US");
console.log(formatter.formatRange(100, 200));
// Output: "100–200"
console.log(formatter.formatRange(100n, 200n));
// Output: "100–200"
console.log(formatter.formatRange("100", "200"));
// Output: "100–200"
Строковые значения преобразуются в числа с сохранением точности, без проблем с плавающей запятой.
Когда использовать formatRange, а когда ручное форматирование
Используйте formatRange(), когда нужно показать диапазоны пользователям. Это актуально для ценовых диапазонов, диапазонов количества, измерений, номеров страниц и любых других ограниченных значений. Метод обеспечивает корректное форматирование с учётом локали, не требуя ручной реализации разделителей.
Не используйте formatRange(), если нужно показать несколько отдельных значений, которые не связаны как диапазон. Например, для списка цен вроде "$100, $150, $200" используйте обычные вызовы format() для каждого значения, а не форматируйте их как диапазон.
Также не используйте formatRange(), если связь между значениями не является числовым диапазоном. Если вы показываете сравнение или разницу, используйте подходящее форматирование для этого контекста, а не форматирование диапазона.