Сравнение дат

Работа с датами в Zod опирается на тип z.date(), который обеспечивает строгую проверку значений Date и позволяет строить сложные правила сравнения: диапазоны времени, ограничения относительно текущего момента, валидацию последовательности дат и контроль бизнес-логики.

Тип z.date() принимает только корректные экземпляры Date. Любые строки, числа или некорректные значения отклоняются без дополнительной обработки.

import { z } from "zod";

const schema = z.date();

schema.parse(new Date()); // корректно
schema.parse("2024-01-01"); // ошибка
schema.parse(123456789); // ошибка

Такое поведение важно для предотвращения неявных преобразований, которые часто становятся источником ошибок в логике сравнения дат.


Сравнение дат: min и max ограничения

Для ограничения диапазона используются методы min и max. Они задают нижнюю и верхнюю границу допустимого значения.

const schema = z.date({
  required_error: "Дата обязательна"
}).min(new Date("2020-01-01")).max(new Date("2030-01-01"));

Если значение выходит за пределы диапазона, схема возвращает ошибку валидации.


Использование gte, lte, gt, lt

Для более точного контроля применяются операторы:

  • gte — больше или равно
  • lte — меньше или равно
  • gt — строго больше
  • lt — строго меньше
const schema = z.date().gte(new Date("2022-01-01")).lt(new Date("2023-01-01"));

Такой подход удобен при моделировании бизнес-правил, например ограничений периода регистрации или действия лицензии.


Сравнение с текущей датой

Частый сценарий — проверка относительно текущего момента. В этом случае важно учитывать, что new Date() вычисляется при создании схемы, а не при каждом вызове parse.

const now = new Date();

const schema = z.date().max(now);

Если требуется динамическое сравнение, используется refine.

const schema = z.date().refine((date) => date <= new Date(), {
  message: "Дата не может быть в будущем"
});

Сравнение двух дат внутри объекта

При работе с сущностями часто требуется проверить взаимосвязь нескольких дат: начало и конец периода.

const schema = z.object({
  start: z.date(),
  end: z.date()
}).refine((data) => data.end > data.start, {
  message: "Дата окончания должна быть позже даты начала",
  path: ["end"]
});

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


Использование superRefine для сложной логики

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

const schema = z.object({
  start: z.date(),
  end: z.date()
}).superRefine((data, ctx) => {
  if (data.end <= data.start) {
    ctx.addIssue({
      path: ["end"],
      message: "end должен быть позже start",
      code: z.ZodIssueCode.custom
    });
  }

  const diff = data.end.getTime() - data.start.getTime();
  const days = diff / (1000 * 60 * 60 * 24);

  if (days > 30) {
    ctx.addIssue({
      path: ["end"],
      message: "Период не может превышать 30 дней",
      code: z.ZodIssueCode.custom
    });
  }
});

Преобразование строк в Date перед сравнением

В реальных API часто приходят строки ISO-формата. Для этого используется preprocess или coerce.

const schema = z.object({
  date: z.coerce.date().min(new Date("2020-01-01"))
});

z.coerce.date() автоматически преобразует строку в Date перед проверкой.

Эквивалент через preprocess:

const schema = z.object({
  date: z.preprocess((val) => new Date(val), z.date())
});

Сравнение дат с учётом времени суток

Date в JavaScript включает временную часть, поэтому сравнение может давать неожиданные результаты, если не нормализовать значения.

const normalize = (d) => new Date(d.setHours(0, 0, 0, 0));

const schema = z.object({
  date: z.date().refine((d) => normalize(d) >= normalize(new Date("2024-01-01")))
});

Это важно при работе с календарными событиями, где время суток не имеет значения.


Проверка пересечения интервалов

Распространённая задача — определить пересечение двух диапазонов дат.

const intervalSchema = z.object({
  start: z.date(),
  end: z.date()
});

const schema = intervalSchema.refine((data) => data.end > data.start);

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

const hasOverlap = (a, b) =>
  a.start <= b.end && b.start <= a.end;

Комбинирование с трансформациями

После проверки даты часто требуется преобразование в timestamp или ISO-строку.

const schema = z.date().transform((date) => date.toISOString());

Или в Unix timestamp:

const schema = z.date().transform((date) => date.getTime());

Типовые ошибки при сравнении дат

Некорректная работа с датами чаще всего связана с:

  • сравнением строк вместо Date
  • отсутствием нормализации времени
  • использованием new Date() внутри схемы без учёта динамики
  • неявным приведением типов

Использование строгой схемы через Zod устраняет часть этих проблем за счёт явной типизации и централизованной валидации.


Сравнение дат в контексте бизнес-правил

При моделировании доменных сущностей даты часто участвуют в ограничениях:

  • дата начала подписки не может быть в прошлом
  • дата окончания должна быть позже начала
  • срок действия не должен превышать лимит
  • события должны попадать в допустимый календарный диапазон
const subscriptionSchema = z.object({
  start: z.date(),
  end: z.date()
}).superRefine((data, ctx) => {
  const now = new Date();

  if (data.start < now) {
    ctx.addIssue({
      path: ["start"],
      message: "Начало подписки не может быть в прошлом",
      code: z.ZodIssueCode.custom
    });
  }

  if (data.end <= data.start) {
    ctx.addIssue({
      path: ["end"],
      message: "Окончание должно быть позже начала",
      code: z.ZodIssueCode.custom
    });
  }
});

Поведение при сериализации и десериализации

При передаче данных через JSON даты становятся строками, что требует обязательного преобразования обратно в Date до сравнения.

const input = {
  start: "2024-01-01T10:00:00Z",
  end: "2024-01-10T10:00:00Z"
};

const schema = z.object({
  start: z.coerce.date(),
  end: z.coerce.date()
});

schema.parse(input);

Без коэрсинга сравнение через gte/lte становится невозможным из-за несоответствия типов.


Сравнение дат в массивах

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

const eventSchema = z.array(z.date()).refine((dates) => {
  for (let i = 1; i < dates.length; i++) {
    if (dates[i] < dates[i - 1]) return false;
  }
  return true;
});

Итеративное уточнение правил

Сложные сценарии сравнения дат обычно строятся постепенно:

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

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