Как форматировать диапазоны дат, например, 1 янв. – 5 янв.

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

Введение

Диапазоны дат встречаются во многих веб-приложениях. Системы бронирования показывают доступность с 1 января по 5 января, календари событий отображают многодневные конференции, аналитические панели показывают данные за определенные периоды, а отчеты охватывают финансовые кварталы или пользовательские временные интервалы. Эти диапазоны указывают, что что-то относится ко всем датам между двумя конечными точками.

Когда вы форматируете диапазоны дат вручную, объединяя две отформатированные даты с помощью дефиса, вы создаете излишне многословный вывод. Диапазон с 1 января 2024 года по 5 января 2024 года не требует повторения месяца и года во второй дате. Вывод "1–5 января 2024 года" передает ту же информацию более лаконично, исключая избыточные компоненты.

JavaScript предоставляет метод formatRange() в Intl.DateTimeFormat для автоматической обработки форматирования диапазонов дат. Этот метод определяет, какие компоненты даты включать в каждую часть диапазона, применяя локализованные соглашения для разделителей и форматирования, одновременно устраняя ненужное повторение.

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

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

Для диапазона в пределах одного дня нужно показывать только разницу во времени: "10:00 - 14:00". Повторение полной даты для обоих временных интервалов не добавляет информации.

Для диапазона в пределах одного месяца указывается месяц один раз, а номера дней перечисляются: "1–5 января 2024 года". Повторение "января" дважды делает вывод труднее читаемым, не добавляя ясности.

Для диапазона, охватывающего разные месяцы в одном году, указываются оба месяца, но год можно опустить в первой дате: "25 декабря 2024 года – 2 января 2025 года" требует полной информации, но "15 января – 20 февраля 2024 года" может опустить год в первой дате в некоторых локалях.

Для диапазонов, охватывающих несколько лет, необходимо указывать год для обеих дат: "1 декабря 2023 года – 15 марта 2024 года".

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

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

Метод formatRange() принимает два объекта Date и возвращает отформатированную строку. Создайте экземпляр Intl.DateTimeFormat с желаемой локалью и параметрами, затем вызовите formatRange() с начальной и конечной датами.

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

const start = new Date(2024, 0, 1);
const end = new Date(2024, 0, 5);

console.log(formatter.formatRange(start, end));
// Вывод: "1/1/24 – 1/5/24"

Форматировщик применяет стандартный формат даты для английского языка США к обеим датам и соединяет их с помощью длинного тире. Для дат в пределах одного месяца вывод по-прежнему показывает обе полные даты, так как стандартный формат достаточно короткий.

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

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const start = new Date(2024, 0, 1);
const end = new Date(2024, 0, 5);

console.log(formatter.formatRange(start, end));
// Вывод: "January 1 – 5, 2024"

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

Как formatRange оптимизирует вывод диапазона дат

Метод formatRange() анализирует обе даты и определяет, какие компоненты различаются. Он включает только необходимые компоненты в каждую часть диапазона.

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

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(formatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Вывод: "January 1 – 5, 2024"

console.log(formatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 0, 20)
));
// Вывод: "January 15 – 20, 2024"

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

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

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(formatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 1, 20)
));
// Вывод: "January 15 – February 20, 2024"

Форматировщик включает оба названия месяца, но размещает год в конце, применяя его ко всему диапазону.

Для дат, охватывающих разные годы, отображаются обе полные даты.

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(formatter.formatRange(
  new Date(2023, 11, 25),
  new Date(2024, 0, 5)
));
// Вывод: "December 25, 2023 – January 5, 2024"

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

Форматирование диапазонов дат и времени

Когда ваш формат включает компоненты времени, метод formatRange() применяет тот же интеллектуальный подход к пропуску полей времени.

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

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "numeric",
  minute: "numeric"
});

const start = new Date(2024, 0, 1, 10, 0);
const end = new Date(2024, 0, 1, 14, 30);

console.log(formatter.formatRange(start, end));
// Вывод: "January 1, 2024, 10:00 AM – 2:30 PM"

Дата отображается один раз, за ней следует диапазон времени. Форматировщик понимает, что повторение полной даты и времени для конца не добавит полезной информации.

Для времени в разные дни отображаются как полные значения даты, так и времени.

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric",
  hour: "numeric",
  minute: "numeric"
});

const start = new Date(2024, 0, 1, 10, 0);
const end = new Date(2024, 0, 2, 14, 30);

console.log(formatter.formatRange(start, end));
// Вывод: "January 1, 2024, 10:00 AM – January 2, 2024, 2:30 PM"

Обе даты и время отображаются, так как они представляют разные дни. Форматировщик не может безопасно пропустить какие-либо компоненты.

Форматирование диапазонов дат в разных локалях

Форматирование диапазонов адаптируется к соглашениям каждой локали относительно порядка компонентов даты, разделителей и того, какие компоненты можно опустить.

const enFormatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(enFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Вывод: "January 1 – 5, 2024"

const deFormatter = new Intl.DateTimeFormat("de-DE", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(deFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Вывод: "1.–5. Januar 2024"

const jaFormatter = new Intl.DateTimeFormat("ja-JP", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(jaFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Вывод: "2024年1月1日~5日"

Английский язык ставит месяц первым и показывает год в конце. Немецкий язык ставит номера дней первыми с точками, затем название месяца, затем год. Японский использует порядок год-месяц-день и волнистую черту (~) в качестве разделителя диапазона. Каждая локаль применяет свои собственные соглашения о том, какие компоненты показывать один раз, а какие дважды.

Эти различия распространяются на диапазоны, охватывающие разные месяцы.

const enFormatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(enFormatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 1, 20)
));
// Вывод: "January 15 – February 20, 2024"

const deFormatter = new Intl.DateTimeFormat("de-DE", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(deFormatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 1, 20)
));
// Вывод: "15. Januar – 20. Februar 2024"

const frFormatter = new Intl.DateTimeFormat("fr-FR", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

console.log(frFormatter.formatRange(
  new Date(2024, 0, 15),
  new Date(2024, 1, 20)
));
// Вывод: "15 janvier – 20 février 2024"

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

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

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

const shortFormatter = new Intl.DateTimeFormat("ru-RU", {
  dateStyle: "short"
});

console.log(shortFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Результат: "01.01.24 – 05.01.24"

const mediumFormatter = new Intl.DateTimeFormat("ru-RU", {
  dateStyle: "medium"
});

console.log(mediumFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Результат: "1 янв. – 5 янв. 2024 г."

const longFormatter = new Intl.DateTimeFormat("ru-RU", {
  dateStyle: "long"
});

console.log(longFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Результат: "1 января – 5 января 2024 г."

const fullFormatter = new Intl.DateTimeFormat("ru-RU", {
  dateStyle: "full"
});

console.log(fullFormatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
));
// Результат: "понедельник, 1 января – пятница, 5 января 2024 г."

Стиль short создает числовые даты и не применяет интеллектуальное упрощение, так как формат уже компактный. Стиль medium и long сокращает или полностью пишет название месяца и опускает избыточные компоненты. Стиль full включает названия дней недели для обеих дат.

Использование formatRangeToParts для пользовательского оформления

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

const formatter = new Intl.DateTimeFormat("ru-RU", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const parts = formatter.formatRangeToParts(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
);

console.log(parts);

Результат — массив объектов, каждый из которых имеет свойства type, value и source.

[
  { type: "month", value: "января", source: "startRange" },
  { type: "literal", value: " ", source: "startRange" },
  { type: "day", value: "1", source: "startRange" },
  { type: "literal", value: " – ", source: "shared" },
  { type: "day", value: "5", source: "endRange" },
  { type: "literal", value: " ", source: "shared" },
  { type: "year", value: "2024", source: "shared" }
]

Свойство type определяет компонент: месяц, день, год или текст. Свойство value содержит отформатированный текст. Свойство source указывает, относится ли компонент к начальной дате, конечной дате или является общим для обеих.

Вы можете использовать эти части для создания пользовательского HTML с оформлением для различных компонентов.

const formatter = new Intl.DateTimeFormat("ru-RU", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const parts = formatter.formatRangeToParts(
  new Date(2024, 0, 1),
  new Date(2024, 0, 5)
);

let html = "";

parts.forEach(part => {
  if (part.type === "month") {
    html += `<span class="month">${part.value}</span>`;
  } else if (part.type === "day") {
    html += `<span class="day">${part.value}</span>`;
  } else if (part.type === "year") {
    html += `<span class="year">${part.value}</span>`;
  } else if (part.type === "literal" && part.source === "shared" && part.value.includes("–")) {
    html += `<span class="separator">${part.value}</span>`;
  } else {
    html += part.value;
  }
});

console.log(html);
// Результат: <span class="month">января</span> <span class="day">1</span><span class="separator"> – </span><span class="day">5</span> <span class="year">2024</span>

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

Что происходит, когда даты равны

Когда вы передаете одну и ту же дату для параметров начала и конца, formatRange() выводит одну отформатированную дату вместо диапазона.

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const date = new Date(2024, 0, 1);

console.log(formatter.formatRange(date, date));
// Вывод: "January 1, 2024"

Форматировщик распознает, что диапазон с одинаковыми начальной и конечной точками не является настоящим диапазоном, и форматирует его как одну дату. Это поведение применяется даже тогда, когда объекты Date являются разными экземплярами с одинаковым значением.

const formatter = new Intl.DateTimeFormat("en-US", {
  year: "numeric",
  month: "long",
  day: "numeric"
});

const start = new Date(2024, 0, 1, 10, 0);
const end = new Date(2024, 0, 1, 10, 0);

console.log(formatter.formatRange(start, end));
// Вывод: "January 1, 2024"

Хотя это отдельные объекты Date, они представляют одну и ту же дату и время. Форматировщик выводит одну дату, так как диапазон имеет нулевую продолжительность на уровне точности заданных параметров формата. Поскольку формат не включает компоненты времени, время не имеет значения, и даты считаются равными.

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

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

Избегайте использования formatRange(), если вам нужно отображать несколько несвязанных дат. Список дедлайнов, например, "January 1, January 15, February 1", должен использовать обычные вызовы format() для каждой даты, а не рассматривать их как диапазоны.

Также избегайте использования formatRange() при отображении сравнений или различий между датами. Если вы показываете, насколько раньше или позже одна дата по сравнению с другой, это представляет собой расчет относительного времени, а не диапазон.