Парсинг неоднозначных дат

Природа неоднозначности при разборе дат

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

  • локальные форматы дат (например, 01/02/03)
  • отсутствие информации о часовом поясе
  • сокращённые или неполные представления времени
  • различия между порядком компонентов (день/месяц/год против месяц/день/год)
  • неявные значения полей (время, день недели, миллисекунды)

При парсинге таких значений библиотека должна либо выбрать стратегию интерпретации, либо сигнализировать об ошибке.

В экосистеме js-joda эта задача решается через комбинацию форматтеров и правил разрешения неоднозначностей.


Базовый механизм парсинга

Основной вход в систему разбора — это методы parse у типов даты и времени:

  • LocalDate.parse
  • LocalDateTime.parse
  • ZonedDateTime.parse
  • Instant.parse

Простейший случай предполагает строго ISO-8601 формат:

LocalDate.parse("2026-05-25")
LocalDateTime.parse("2026-05-25T10:15:30")
ZonedDateTime.parse("2026-05-25T10:15:30+05:00[Asia/Almaty]")

ISO-парсинг не допускает неоднозначностей, поскольку формат строго определён.


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

На практике используются нестандартные представления:

  • 25.05.26
  • 05/06/07
  • 2026/05/25 10:15
  • 01-02-03

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

Для их обработки используется DateTimeFormatter.


DateTimeFormatter как инструмент управления интерпретацией

Форматтер задаёт структуру входной строки:

const formatter = DateTimeFormatter.ofPattern("dd/MM/yy");
const date = LocalDate.parse("25/05/26", formatter);

Ключевая особенность — порядок компонентов фиксируется шаблоном. Это устраняет базовую неоднозначность.

Однако остаются ситуации, когда данные формально соответствуют шаблону, но допускают несколько трактовок:

  • двухзначный год
  • отсутствие времени
  • пропущенные поля
  • конфликт календарных значений

Двузначные годы и расширение диапазона

При использовании шаблонов вида yy возникает неопределённость:

25/05/26 → 1926 или 2026

js-joda применяет расширение года через базовое смещение, задаваемое форматтером:

const formatter = DateTimeFormatter
  .ofPattern("dd/MM/yy")
  .withDefaultYear(2026);

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

  • 00–99 → преобразование в полный год
  • правило зависит от конфигурации formatter’а

ResolverStyle и стратегии разрешения неоднозначностей

Ключевой механизм управления интерпретацией — ResolverStyle.

Поддерживаются три режима:

STRICT

Строгая проверка всех значений:

  • отклоняются несуществующие даты (например, 31 февраля)
  • запрещены скрытые преобразования
  • отсутствует автоматическое исправление
const formatter = DateTimeFormatter
  .ofPattern("dd/MM/uuuu")
  .withResolverStyle(ResolverStyle.STRICT);

Поведение:

  • 2026-02-31 → ошибка
  • 25/13/2026 → ошибка

SMART

Компромиссный режим с логическими поправками:

  • корректирует валидные, но некорректные значения
  • например, 31 апреля может быть преобразовано в 30 апреля
.withResolverStyle(ResolverStyle.SMART);

Особенности:

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

LENIENT

Максимально свободная интерпретация:

  • перенос избыточных значений
  • нормализация переполнений (месяцев, дней)

Пример поведения:

2026-13-01 → январь 2027

Такой режим фактически превращает парсер в арифметический преобразователь дат.


Неоднозначность порядка компонентов

Наиболее частая проблема — различие форматов:

  • dd/MM/yyyy (европейский стиль)
  • MM/dd/yyyy (американский стиль)

Строка:

01/02/2026

может означать:

  • 1 февраля
  • 2 января

js-joda не пытается угадывать формат. Интерпретация полностью определяется DateTimeFormatter.

const euro = DateTimeFormatter.ofPattern("dd/MM/uuuu");
const us = DateTimeFormatter.ofPattern("MM/dd/uuuu");

LocalDate.parse("01/02/2026", euro);
LocalDate.parse("01/02/2026", us);

Частично заданные значения и заполнение полей

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

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

Пример:

LocalDateTime.parse("2026-05-25T10:15");

Если часть данных отсутствует, используются:

  • значения по умолчанию форматтера
  • или ошибка, если контекст требует полноты

Парсинг временных зон и скрытая неоднозначность

Часовые пояса создают отдельный класс проблем:

2026-05-25T10:00
2026-05-25T10:00Z
2026-05-25T10:00+05:00

Различия:

  • отсутствие зоны → локальное время
  • Z → UTC
  • смещение → фиксированная зона

При преобразовании в ZonedDateTime возникает необходимость выбора зоны по умолчанию:

const zdt = LocalDateTime
  .parse("2026-05-25T10:15")
  .atZone(ZoneId.of("Asia/Almaty"));

Неоднозначность устраняется только явным указанием зоны.


Автоматическое восстановление значений

При использовании SMART или LENIENT возможны автоматические преобразования:

  • переполнение минут → увеличение часов
  • переполнение дней → перенос месяца
  • некорректные даты → нормализация

Пример логики:

2026-05-25T25:10 → 2026-05-26T01:10

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


Разрешение полей через Resolver

Внутренний механизм js-joda использует концепцию resolver’а:

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

При конфликте полей:

  • STRICT → ошибка
  • SMART → попытка согласования
  • LENIENT → арифметическое преобразование

Конфликтующие значения полей

Пример конфликтов:

  • день недели не соответствует дате
  • месяц выходит за диапазон
  • отрицательные значения

При разборе:

DateTimeFormatter.ofPattern("e uuuu MM dd")

возможна ситуация, когда e (день недели) противоречит dd/MM.

Решение зависит от режима:

  • STRICT — отклонение
  • SMART — игнорирование или коррекция
  • LENIENT — пересчёт даты

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

Локаль влияет на:

  • порядок компонентов
  • названия месяцев
  • сокращения дней недели
DateTimeFormatter.ofPattern("d MMM uuuu")
  .withLocale(Locale.FRANCE);

Строка:

25 mai 2026

интерпретируется корректно только при совпадении локали.

При несовпадении возникает неоднозначность или ошибка парсинга.


Ошибки парсинга как форма контроля неоднозначности

Ошибки в js-joda не являются побочным эффектом, а выступают механизмом защиты от неверной интерпретации.

Типичные причины:

  • несовпадение шаблона
  • невозможные календарные значения
  • конфликт полей при STRICT режиме
  • отсутствие обязательных компонентов

Согласование форматов в прикладных сценариях

При работе с внешними источниками данных применяются фиксированные стратегии:

  • жёсткое задание DateTimeFormatter
  • использование STRICT для валидации
  • нормализация входных данных до парсинга
  • разделение локального и UTC времени

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


Итоговые механизмы устранения неоднозначностей

В js-joda устранение неоднозначности достигается сочетанием трёх факторов:

  • фиксированные шаблоны DateTimeFormatter
  • стратегии ResolverStyle
  • явное управление временными зонами

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

  • формат → структура строки
  • resolver → логика интерпретации
  • zone → пространственная привязка времени