Работа с неоднозначными датами

Работа с датами в JavaScript осложняется особенностями локализации, часовых поясов, форматов хранения и преобразования времени. Даже простая строка может интерпретироваться по-разному:

new Date('01/02/2025')

В зависимости от окружения такая дата может означать:

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

Библиотека date-fns предоставляет набор функций, позволяющих явно управлять парсингом, форматированием и преобразованием дат без скрытой магии и мутаций объектов.


Источники неоднозначности дат

Локальные форматы

Наиболее распространённая проблема — различие региональных стандартов:

Формат Значение
MM/dd/yyyy США
dd/MM/yyyy Европа
yyyy-MM-dd ISO 8601

Строка:

03/04/2025

может быть:

  • 3 апреля
  • 4 марта

Часовые пояса

Дата без времени:

2025-05-10

может интерпретироваться как:

2025-05-10T00:00:00Z

или как локальное время пользователя.

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

Неполные даты

Строки:

2025-05
2025
05-10

не содержат полного набора компонентов и интерпретируются неодинаково.

Невалидные даты

JavaScript допускает создание некорректных значений:

new Date('2025-13-99')

Результат:

Invalid Date

Использование parse вместо Date constructor

Конструктор Date не гарантирует стабильный парсинг нестандартных форматов.

Проблемный пример:

new Date('10/11/2025')

Безопаснее использовать parse.


Функция parse

import { parse } from 'date-fns'

Синтаксис:

parse(dateString, formatString, referenceDate)

Пример:

const result = parse(
  '25/12/2025',
  'dd/MM/yyyy',
  new Date()
)

console.log(result)

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


Явное описание структуры даты

Европейский формат

parse('31/01/2025', 'dd/MM/yyyy', new Date())

Американский формат

parse('01/31/2025', 'MM/dd/yyyy', new Date())

ISO-формат

parse('2025-01-31', 'yyyy-MM-dd', new Date())

referenceDate и его роль

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

Пример:

parse('15:30', 'HH:mm', new Date())

Строка содержит только время. Год, месяц и день будут взяты из referenceDate.


Проблема скрытых значений

const ref = new Date(2025, 0, 1)

const result = parse('18:45', 'HH:mm', ref)

Результат:

2025-01-01T18:45:00

Дата появилась автоматически из referenceDate.


Проверка валидности даты

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


isValid

import { isValid } from 'date-fns'

Пример:

const date = parse(
  '31/02/2025',
  'dd/MM/yyyy',
  new Date()
)

console.log(isValid(date))

Результат:

false

Строгий контроль формата

Date-fns строго следует указанным токенам.


Неверный формат

parse(
  '2025/12/31',
  'dd-MM-yyyy',
  new Date()
)

Результат будет невалидным.


Частая ошибка с yyyy и YYYY

yyyy

Календарный год:

format(date, 'yyyy')

YYYY

Week-numbering year.

Использование YYYY способно приводить к неожиданным результатам на границе года.

Правильно:

format(date, 'yyyy-MM-dd')

Работа с ISO-датами

ISO 8601 — наиболее безопасный формат хранения.

Пример:

2025-05-20T14:30:00Z

parseISO

import { parseISO } from 'date-fns'

Пример:

const date = parseISO('2025-05-20T14:30:00Z')

Почему parseISO безопаснее

Функция:

  • понимает ISO-форматы
  • корректно обрабатывает UTC
  • исключает региональные неоднозначности

UTC и локальное время


Проблема смещения даты

parseISO('2025-05-20')

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


Разница между UTC и local time

Дата:

2025-05-20T00:00:00Z

означает полночь UTC.

В часовом поясе UTC-5 это:

2025-05-19 19:00

Форматирование неоднозначных дат


format

import { format } from 'date-fns'

Пример:

format(date, 'dd.MM.yyyy')

Результат:

20.05.2025

Безопасные пользовательские форматы

Для хранения

Рекомендуется:

yyyy-MM-dd

или:

yyyy-MM-dd'T'HH:mm:ssXXX

Для отображения

dd.MM.yyyy

или:

MMMM d, yyyy

Разбор пользовательского ввода

Пользовательский ввод всегда считается ненадёжным.


Непредсказуемые разделители

Пользователь может ввести:

10-05-2025
10/05/2025
10.05.2025

Нормализация строки

Пример:

const normalized = input.replace(/[.-]/g, '/')

После этого:

parse(normalized, 'dd/MM/yyyy', new Date())

Обработка неполных дат


Только месяц и год

parse('05/2025', 'MM/yyyy', new Date())

День будет взят из referenceDate.


Явная установка дня

Часто безопаснее вручную задавать начало месяца:

import { startOfMonth } from 'date-fns'

const parsed = parse(
  '05/2025',
  'MM/yyyy',
  new Date()
)

const normalized = startOfMonth(parsed)

Неоднозначность при сравнении дат


Проблема времени

Две даты могут выглядеть одинаково:

2025-05-20

но содержать разное время.


isEqual

import { isEqual } from 'date-fns'

Сравнение:

isEqual(date1, date2)

учитывает время полностью.


Сравнение только календарной даты

import { isSameDay } from 'date-fns'

Пример:

isSameDay(date1, date2)

Начало и конец суток

Часто неоднозначность возникает при фильтрации диапазонов.


startOfDay

import { startOfDay } from 'date-fns'

endOfDay

import { endOfDay } from 'date-fns'

Пример фильтрации

const start = startOfDay(date)
const end = endOfDay(date)

Диапазоны дат


Интерпретация конца диапазона

Дата:

2025-05-20

как конец диапазона может означать:

  • начало дня
  • конец дня

Правильная нормализация

const from = startOfDay(startDate)
const to = endOfDay(endDate)

Работа с часовыми поясами

Базовая версия Date-fns не содержит полноценной timezone-системы.

Для этого используется:

date-fns-tz

Преобразование временных зон

import { utcToZonedTime } from 'date-fns-tz'

Пример:

const zoned = utcToZonedTime(
  '2025-05-20T12:00:00Z',
  'Europe/Berlin'
)

Летнее и зимнее время

DST — источник множества ошибок.


Несуществующее время

В некоторых странах время:

02:30

может не существовать в день перевода часов.


Повторяющееся время

При переходе назад одно и то же локальное время возникает дважды.


Безопасная стратегия хранения дат


Рекомендуемый подход

На сервере

Хранение в UTC:

2025-05-20T14:00:00Z

В интерфейсе

Преобразование в локальный timezone пользователя.


parseJSON


Назначение

Функция предназначена для разбора JSON-дат.

import { parseJSON } from 'date-fns'

Пример

parseJSON('2025-05-20T14:00:00.000Z')

Локализация и неоднозначность


Форматирование с locale

import { format } from 'date-fns'
import { ru } from 'date-fns/locale'

Пример:

format(date, 'd MMMM yyyy', {
  locale: ru
})

Почему локализация влияет на неоднозначность

Формат:

05/10/2025

без locale непонятен.

Формат:

10 мая 2025

не содержит неоднозначности.


Практические рекомендации

Использовать ISO для хранения

yyyy-MM-dd

или:

yyyy-MM-dd'T'HH:mm:ssXXX

Не использовать Date.parse для нестандартных форматов

Проблемно:

Date.parse('10/11/2025')

Всегда задавать formatString явно

Безопасно:

parse(
  input,
  'dd/MM/yyyy',
  new Date()
)

Проверять результат через isValid

if (!isValid(date)) {
  throw new Error('Некорректная дата')
}

Разделять хранение и отображение

Хранение

UTC + ISO.

Отображение

Локализованный format.


Избегать скрытого времени

Если важна только календарная дата:

startOfDay(date)

Не сравнивать даты через строки

Плохо:

date1.toString() === date2.toString()

Лучше:

isSameDay(date1, date2)

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

import {
  parse,
  isValid,
  startOfDay,
  format
} from 'date-fns'

function normalizeUserDate(input) {
  const normalized = input
    .trim()
    .replace(/[.-]/g, '/')

  const parsed = parse(
    normalized,
    'dd/MM/yyyy',
    new Date()
  )

  if (!isValid(parsed)) {
    throw new Error('Invalid date')
  }

  return startOfDay(parsed)
}

const result = normalizeUserDate(
  '20.05.2025'
)

console.log(
  format(result, 'yyyy-MM-dd')
)

Этот подход:

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