Работа с датами в JavaScript осложняется особенностями локализации, часовых поясов, форматов хранения и преобразования времени. Даже простая строка может интерпретироваться по-разному:
new Date('01/02/2025')
В зависимости от окружения такая дата может означать:
Библиотека date-fns предоставляет набор функций, позволяющих явно управлять парсингом, форматированием и преобразованием дат без скрытой магии и мутаций объектов.
Наиболее распространённая проблема — различие региональных стандартов:
| Формат | Значение |
|---|---|
MM/dd/yyyy |
США |
dd/MM/yyyy |
Европа |
yyyy-MM-dd |
ISO 8601 |
Строка:
03/04/2025
может быть:
Дата без времени:
2025-05-10
может интерпретироваться как:
2025-05-10T00:00:00Z
или как локальное время пользователя.
При преобразовании между часовыми поясами дата способна сместиться на сутки.
Строки:
2025-05
2025
05-10
не содержат полного набора компонентов и интерпретируются неодинаково.
JavaScript допускает создание некорректных значений:
new Date('2025-13-99')
Результат:
Invalid Date
Конструктор Date не гарантирует стабильный парсинг
нестандартных форматов.
Проблемный пример:
new Date('10/11/2025')
Безопаснее использовать 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())
parse('2025-01-31', 'yyyy-MM-dd', new Date())
Третий аргумент используется как источник недостающих компонентов.
Пример:
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.
После парсинга всегда рекомендуется проверять результат.
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()
)
Результат будет невалидным.
Календарный год:
format(date, 'yyyy')
Week-numbering year.
Использование YYYY способно приводить к неожиданным
результатам на границе года.
Правильно:
format(date, 'yyyy-MM-dd')
ISO 8601 — наиболее безопасный формат хранения.
Пример:
2025-05-20T14:30:00Z
import { parseISO } from 'date-fns'
Пример:
const date = parseISO('2025-05-20T14:30:00Z')
Функция:
parseISO('2025-05-20')
В некоторых часовых поясах результат может визуально отображаться как предыдущий день.
Дата:
2025-05-20T00:00:00Z
означает полночь UTC.
В часовом поясе UTC-5 это:
2025-05-19 19:00
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
но содержать разное время.
import { isEqual } from 'date-fns'
Сравнение:
isEqual(date1, date2)
учитывает время полностью.
import { isSameDay } from 'date-fns'
Пример:
isSameDay(date1, date2)
Часто неоднозначность возникает при фильтрации диапазонов.
import { startOfDay } from 'date-fns'
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 пользователя.
Функция предназначена для разбора JSON-дат.
import { parseJSON } from 'date-fns'
parseJSON('2025-05-20T14:00:00.000Z')
import { format } from 'date-fns'
import { ru } from 'date-fns/locale'
Пример:
format(date, 'd MMMM yyyy', {
locale: ru
})
Формат:
05/10/2025
без locale непонятен.
Формат:
10 мая 2025
не содержит неоднозначности.
yyyy-MM-dd
или:
yyyy-MM-dd'T'HH:mm:ssXXX
Проблемно:
Date.parse('10/11/2025')
Безопасно:
parse(
input,
'dd/MM/yyyy',
new Date()
)
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')
)
Этот подход: