Парсинг отформатированных дат

Работа с датами в JavaScript традиционно делится на две отдельные задачи: форматирование и интерпретация строк. Встроенный Intl API предоставляет мощные инструменты для локализованного отображения дат, но обратное преобразование — парсинг уже отформатированной строки обратно в структурированные компоненты — не имеет прямого универсального решения.

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

  • 26.05.2026 (ru-RU)
  • 05/26/2026 (en-US)
  • 2026-05-26 (ISO-подобный формат)
  • 26 mai 2026 (fr-FR)

Стандарт ECMAScript не предоставляет метода вида Intl.DateTimeFormat.parse(), поэтому обратное преобразование строится через косвенные механизмы.


Структура Intl.DateTimeFormat и форматирование

Базовый инструмент для локализованного вывода дат — Intl.DateTimeFormat:

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

formatter.format(new Date(2026, 4, 26)); // "26.05.2026"

Этот API работает только в одну сторону: из Date → строка.

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

formatToParts()

formatToParts как основа обратного разбора

Метод formatToParts() возвращает структуру, а не строку. Вместо единого текста он выдаёт массив компонентов:

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

formatter.formatToParts(new Date(2026, 4, 26));

Результат:

[
  { type: 'day', value: '26' },
  { type: 'literal', value: '.' },
  { type: 'month', value: '05' },
  { type: 'literal', value: '.' },
  { type: 'year', value: '2026' }
]

Эта структура является ключом к обратному преобразованию, поскольку:

  • type: 'day', 'month', 'year' содержат смысловые данные
  • literal описывает разделители и формат локали

Построение парсера через шаблон локали

Парсинг форматированной даты обычно строится в два шага:

  1. Получение шаблона через formatToParts()
  2. Построение регулярного выражения или позиционного парсинга

Извлечение структуры формата

function getFormatStructure(locale, options) {
  const dtf = new Intl.DateTimeFormat(locale, options);
  const parts = dtf.formatToParts(new Date(2000, 0, 2));

  return parts.map(p => ({
    type: p.type,
    value: p.value
  }));
}

Использование фиксированной даты позволяет определить порядок компонентов.


Построение регулярного выражения

На основе структуры можно создать регулярное выражение:

function buildDateParser(locale) {
  const dtf = new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit'
  });

  const sample = dtf.formatToParts(new Date(2000, 0, 2));

  let pattern = '';
  const map = [];

  for (const part of sample) {
    if (part.type === 'year') {
      pattern += '(\\d{4})';
      map.push('year');
    } else if (part.type === 'month') {
      pattern += '(\\d{1,2})';
      map.push('month');
    } else if (part.type === 'day') {
      pattern += '(\\d{1,2})';
      map.push('day');
    } else {
      pattern += part.value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
  }

  const regex = new RegExp(`^${pattern}$`);

  return function parse(dateString) {
    const match = dateString.match(regex);
    if (!match) return null;

    const result = {};
    map.forEach((key, index) => {
      result[key] = Number(match[index + 1]);
    });

    return result;
  };
}

Пример использования:

const parseRU = buildDateParser('ru-RU');

parseRU('26.05.2026');
// { day: 26, month: 5, year: 2026 }

Локализационные различия и нестабильность форматов

Главная сложность парсинга через Intl заключается в отсутствии стабильного формата. Даже внутри одной локали могут изменяться:

  • порядок компонентов
  • разделители (. / - )
  • наличие ведущих нулей
  • сокращения месяцев (в других форматах)

Например:

new Intl.DateTimeFormat('en-GB').format(new Date(2026, 4, 26));
// "26/05/2026"

new Intl.DateTimeFormat('en-US').format(new Date(2026, 4, 26));
// "5/26/2026"

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


Обработка месяцев в текстовом формате

Если используется текстовое представление месяца:

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

пример строки:

May 26, 2026

formatToParts():

[
  { type: 'month', value: 'May' },
  { type: 'literal', value: ' ' },
  { type: 'day', value: '26' },
  { type: 'literal', value: ', ' },
  { type: 'year', value: '2026' }
]

Парсинг требует дополнительной нормализации:

  • сопоставление названий месяцев
  • учёт локали
  • регистра и сокращений

Пример словаря:

const monthMap = {
  January: 0,
  February: 1,
  March: 2,
  April: 3,
  May: 4,
  June: 5,
  July: 6,
  August: 7,
  September: 8,
  October: 9,
  November: 10,
  December: 11
};

Частичная реконструкция даты без регулярных выражений

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

function parseByParts(locale, dateString) {
  const dtf = new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit'
  });

  const skeleton = dtf.formatToParts(new Date(2000, 0, 2));
  const input = dtf.formatToParts(new Date(dateString));

  const result = {};

  for (let i = 0; i < skeleton.length; i++) {
    const s = skeleton[i];
    const v = input[i];

    if (s.type === 'year') result.year = Number(v.value);
    if (s.type === 'month') result.month = Number(v.value);
    if (s.type === 'day') result.day = Number(v.value);
  }

  return result;
}

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


Ограничения Intl при парсинге дат

Использование Intl для обратного преобразования имеет фундаментальные ограничения:

Отсутствие обратного API

Нет стандартизированного метода интерпретации строк дат.

Локаль может меняться динамически

Операционная система и браузер могут менять формат вывода.

Разные уровни детализации

Одна локаль может возвращать:

  • числовой месяц
  • текстовый месяц
  • смешанный формат

Амбигуитеты

Некоторые форматы неоднозначны:

05/06/2026
  • 5 июня или 6 мая

Сопоставление с альтернативными подходами

В реальных приложениях часто комбинируются разные стратегии:

ISO-строки как основной формат хранения

new Date().toISOString();

Плюсы:

  • однозначность
  • стабильность
  • независимость от локали

Библиотеки для парсинга

Хотя задача рассматривается через Intl, на практике часто используются:

  • строгие парсеры форматов
  • токенизированные библиотеки дат

Temporal API (перспективный стандарт)

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


Синтетический подход: гибридный парсер

Комбинация formatToParts() и словарей даёт наиболее устойчивую модель:

function createIntlDateParser(locale) {
  const dtf = new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit'
  });

  return function parse(str) {
    const parts = dtf.formatToParts(new Date(str));

    const result = {};
    for (const p of parts) {
      if (p.type === 'year') result.year = Number(p.value);
      if (p.type === 'month') result.month = Number(p.value);
      if (p.type === 'day') result.day = Number(p.value);
    }

    return result;
  };
}

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


Практическая модель устойчивого парсинга

На уровне архитектуры систем обработки дат выделяются три слоя:

  1. Нормализация входа

    • ISO предпочтителен
    • локализованные строки преобразуются заранее
  2. Локализация вывода

    • Intl.DateTimeFormat
    • formatToParts для анализа структуры
  3. Хранилище

    • числовые timestamp
    • или ISO 8601

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