Временные интервалы

Работа с временными интервалами в FormatJS строится вокруг стандартного API ECMAScript Internationalization API (Intl) и его расширений, которые используются в экосистеме formatjs для унифицированного форматирования дат, относительного времени и диапазонов дат с учётом локализации.

Относительное время (Relative Time)

Относительное время используется для выражений вида «5 минут назад», «через 2 часа», «3 дня назад». В основе лежит Intl.RelativeTimeFormat.

Базовая концепция

Форматирование строится на двух параметрах:

  • числовое значение разницы
  • единица измерения времени
const rtf = new Intl.RelativeTimeFormat('ru', {
  numeric: 'auto'
});

rtf.format(-5, 'minute'); // "5 минут назад"
rtf.format(2, 'hour');    // "через 2 часа"

Поддерживаемые единицы времени

  • second
  • minute
  • hour
  • day
  • week
  • month
  • quarter
  • year

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

Отрицательные и положительные значения

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

rtf.format(-1, 'day'); // "вчера"
rtf.format(1, 'day');  // "завтра"

При использовании numeric: "auto" возможна замена числовых форм на лексические («вчера», «сегодня», «завтра»), если локаль это поддерживает.


Интеграция RelativeTime в FormatJS

В экосистеме FormatJS используется пакет react-intl, который предоставляет обёртки над Intl.RelativeTimeFormat.

Форматирование через React-компоненты

import { FormattedRelativeTime } from 'react-intl';

<FormattedRelativeTime
  value={-10}
  unit="minute"
/>

Автоматическое обновление интервала

Типичный паттерн включает динамический расчёт разницы:

const now = Date.now();
const diff = (timestamp - now) / 1000;

let value;
let unit;

if (Math.abs(diff) < 60) {
  value = Math.round(diff);
  unit = 'second';
} else if (Math.abs(diff) < 3600) {
  value = Math.round(diff / 60);
  unit = 'minute';
} else {
  value = Math.round(diff / 3600);
  unit = 'hour';
}

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

Для корректного отображения временных интервалов используется стратегия нормализации:

  • секунды — до 60
  • минуты — до 60 минут
  • часы — до 24 часов
  • дни — до 7 дней
  • недели — до 4 недель
  • месяцы — до 12 месяцев
  • годы — свыше
function getRelativeTime(diffSeconds) {
  const abs = Math.abs(diffSeconds);

  if (abs < 60) return ['second', diffSeconds];
  if (abs < 3600) return ['minute', diffSeconds / 60];
  if (abs < 86400) return ['hour', diffSeconds / 3600];
  if (abs < 604800) return ['day', diffSeconds / 86400];
  if (abs < 2629800) return ['week', diffSeconds / 604800];
  if (abs < 31557600) return ['month', diffSeconds / 2629800];

  return ['year', diffSeconds / 31557600];
}

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

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

Intl.DateTimeFormat.formatRange

Современный API позволяет форматировать диапазоны дат:

const formatter = new Intl.DateTimeFormat('ru', {
  year: 'numeric',
  month: 'short',
  day: 'numeric'
});

formatter.formatRange(
  new Date(2024, 0, 1),
  new Date(2024, 0, 10)
);

Результат зависит от локали и может быть представлен как:

1–10 янв. 2024

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

В formatjs используется аналогичный механизм через formatDateTimeRange.

import { useIntl } from 'react-intl';

const intl = useIntl();

intl.formatDateTimeRange(
  new Date(2024, 5, 1),
  new Date(2024, 5, 7),
  {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }
);

Разница между относительным временем и диапазонами

Подход Назначение Пример
RelativeTimeFormat Относительно текущего момента «5 минут назад»
DateTimeRange Интервал между датами «1–7 июня 2024»

Локализация временных интервалов

Форматирование зависит от локали:

  • порядок слов
  • грамматические формы
  • склонение числительных
  • сокращения единиц времени
new Intl.RelativeTimeFormat('en').format(-3, 'day'); // "3 days ago"
new Intl.RelativeTimeFormat('ru').format(-3, 'day'); // "3 дня назад"
new Intl.RelativeTimeFormat('ar').format(-3, 'day'); // арабская форма

Кэширование форматтеров

Создание Intl.RelativeTimeFormat и Intl.DateTimeFormat является ресурсоёмким. В высоконагруженных приложениях применяется кэширование:

const cache = new Map();

function getRtf(locale) {
  if (!cache.has(locale)) {
    cache.set(locale, new Intl.RelativeTimeFormat(locale, {
      numeric: 'auto'
    }));
  }
  return cache.get(locale);
}

Особенности числовой нормализации

При работе с временными интервалами важно учитывать:

  • округление значений
  • погрешности таймстемпов
  • переходы между единицами
Math.round(diff / 60); // минуты
Math.floor(diff / 60); // строгая обрезка

Выбор метода влияет на стабильность отображения UI.


Пограничные случаи временных интервалов

Нулевая разница

rtf.format(0, 'second'); // "сейчас"

Очень большие интервалы

При значениях выше года возможны:

  • округление до ближайшего года
  • потеря точности
  • переключение на календарный формат

Переход единиц

При изменении единицы (например, 59 → 60 секунд) возможны скачки текста:

59 секунд назад → 1 минута назад

Использование в React-компонентах FormatJS

Форматирование через hook

import { useIntl } from 'react-intl';

function TimeLabel({ timestamp }) {
  const intl = useIntl();

  const diff = (timestamp - Date.now()) / 1000;

  const [unit, value] = getRelativeTime(diff);

  return intl.formatRelativeTime(value, unit);
}

Оптимизация обновления времени

Для динамических интерфейсов используется периодическое обновление:

setInterval(() => {
  setNow(Date.now());
}, 1000);

Частота обновления зависит от единицы отображения:

  • секунды → 1s
  • минуты → 10–30s
  • часы → 1–5min

Интернациональные особенности выражения времени

Разные языки обрабатывают интервалы по-разному:

  • синтетические формы (русский, немецкий)
  • аналитические формы (английский)
  • контекстные формы (арабский, японский)

FormatJS делегирует обработку правил движку Intl, обеспечивая консистентность.


Комбинирование относительного времени и диапазонов

В интерфейсах часто используется гибридный подход:

  • краткий интервал (RelativeTime)
  • расширенный диапазон (DateTimeRange)
// "5 минут назад"
intl.formatRelativeTime(-5, 'minute');

// "12:00 — 12:05"
intl.formatDateTimeRange(start, end, {
  hour: '2-digit',
  minute: '2-digit'
});

Производственные паттерны

Унификация форматирования

Создание единого слоя:

export function formatTimeDiff(intl, timestamp) {
  const diff = (timestamp - Date.now()) / 1000;
  const [unit, value] = getRelativeTime(diff);

  return intl.formatRelativeTime(value, unit);
}

Абстракция над Intl

Слой форматирования отделяется от UI, что позволяет:

  • менять локаль без изменения логики
  • тестировать без зависимости от браузера
  • переиспользовать функции в разных компонентах