Временные зоны

Работа с временными зонами является одной из наиболее сложных задач при обработке дат в JavaScript-приложениях. Библиотека Joi, как инструмент валидации схем, предоставляет базовые возможности для проверки дат, но корректная интерпретация временных зон требует понимания особенностей форматов времени, поведения Date в JavaScript и принципов стандарта ISO 8601.

Представление даты и времени в JavaScript

В JavaScript объект Date хранит момент времени в виде количества миллисекунд, прошедших с 1 января 1970 года по UTC. Это означает, что внутренне временная зона не хранится, но влияет на отображение и парсинг строковых значений.

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

  • все даты в объекте Date нормализуются к UTC;
  • строковое представление может содержать локальную временную зону;
  • при парсинге строки без указания зоны поведение зависит от реализации и окружения.

Пример:

new Date("2026-05-10T12:00:00Z") // UTC
new Date("2026-05-10T12:00:00+06:00") // с оффсетом

Joi опирается на стандартный объект Date, поэтому все нюансы временных зон остаются на уровне JavaScript.


Базовая валидация дат в Joi

Joi предоставляет тип date(), который используется для проверки значений даты и времени.

import Joi from "joi";

const schema = Joi.object({
  createdAt: Joi.date().required()
});

Поддерживаются различные входные форматы:

  • ISO 8601 строки;
  • timestamp (число миллисекунд);
  • объект Date.

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


ISO 8601 как основной формат работы с временными зонами

Наиболее корректный способ передачи времени — использование ISO 8601.

Примеры:

  • 2026-05-10T10:00:00Z — UTC время;
  • 2026-05-10T10:00:00+06:00 — смещение относительно UTC;
  • 2026-05-10T10:00:00-03:00 — отрицательное смещение.

Joi корректно принимает такие строки при использовании Joi.date().iso():

const schema = Joi.object({
  eventTime: Joi.date().iso().required()
});

Ключевая особенность iso() заключается в строгой проверке формата строки. Любые нестандартные представления даты будут отклонены.


Влияние временной зоны на валидацию

Joi не интерпретирует временные зоны как бизнес-логику. Она лишь проверяет корректность формата и возможность преобразования в Date.

Например:

Joi.date().validate("2026-05-10T12:00:00+06:00");
Joi.date().validate("2026-05-10T06:00:00Z");

Обе строки описывают один и тот же момент времени, но с разным представлением.

Важный момент:

  • Joi не нормализует временные зоны;
  • Joi не выполняет конвертацию между часовыми поясами;
  • Joi не хранит информацию о зоне после парсинга.

После валидации остаётся только UTC-момент.


Проблема потери информации о временной зоне

При использовании стандартного Date информация о исходной временной зоне теряется.

const value = new Date("2026-05-10T10:00:00+06:00");

После создания объекта:

  • сохраняется только абсолютное время;
  • смещение +06:00 больше недоступно.

Joi не способен восстановить это значение, так как работает поверх Date.


Проверка обязательности UTC формата

Иногда требуется строгое использование UTC, чтобы избежать неоднозначности.

const schema = Joi.object({
  timestamp: Joi.date().iso().required()
    .messages({
      "date.format": "Требуется ISO 8601 формат"
    })
});

Но iso() не гарантирует наличие Z. Строка с +06:00 также будет считаться валидной.

Для строгого UTC обычно используют дополнительную проверку:

const schema = Joi.string().pattern(/Z$/).custom((value, helpers) => {
  const date = new Date(value);
  if (isNaN(date.getTime())) {
    return helpers.error("any.invalid");
  }
  return value;
});

Нормализация времени перед валидацией

Часто данные приводятся к UTC до попадания в Joi.

function toUTC(dateString) {
  return new Date(dateString).toISOString();
}

const schema = Joi.object({
  createdAt: Joi.date().iso().required()
});

Такой подход позволяет:

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

Работа с timestamp и временными зонами

Joi поддерживает числовые значения времени:

Joi.date().timestamp()

Пример:

const schema = Joi.object({
  createdAt: Joi.date().timestamp().required()
});

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

  • timestamp всегда интерпретируется как UTC;
  • временная зона не участвует в вычислении;
  • значение полностью лишено контекста локали.

Это делает timestamp наиболее безопасным форматом для распределённых систем.


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

При необходимости строгого контроля временных зон используется .custom():

const schema = Joi.object({
  date: Joi.string().custom((value, helpers) => {
    const match = value.match(/([+-]\d{2}:\d{2}|Z)$/);
    if (!match) {
      return helpers.error("any.invalid");
    }
    return value;
  })
});

Такой подход позволяет:

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

Временные зоны и сравнение дат

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

const schema = Joi.object({
  start: Joi.date().required(),
  end: Joi.date().greater(Joi.ref("start")).required()
});

Важно понимать:

  • сравнение происходит в UTC;
  • временная зона не влияет на результат;
  • оба значения приводятся к миллисекундам.

Частые ошибки при работе с временными зонами

Основные проблемы возникают не в Joi, а в логике приложения:

  1. Передача локального времени без указания зоны "2026-05-10 12:00:00"

  2. Смешивание форматов ISO + timestamp + локальные строки

  3. Потеря смещения при сериализации Date.toString() вместо toISOString()

  4. Предположение о сохранении временной зоны внутри Date

Joi лишь фиксирует корректность структуры, но не предотвращает архитектурные ошибки.


Рекомендации по проектированию схем

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

  • использование ISO 8601 как единого стандарта;
  • предпочтение UTC в хранении;
  • минимизация локальных форматов;
  • явная проверка строковых форматов при необходимости контроля зоны;
  • использование timestamp в распределённых системах.

Сочетание Joi с библиотеками для временных зон

Для более сложной работы с часовыми поясами часто используется дополнительный слой:

  • Luxon
  • date-fns-tz
  • Day.js с плагинами

Joi при этом выполняет только функцию первичной валидации:

const schema = Joi.object({
  date: Joi.string().required()
});

const value = schema.validate(input);

const parsed = DateTime.fromISO(value.date, { zone: "Asia/Almaty" });

Такое разделение ответственности позволяет:

  • не перегружать Joi логикой преобразования;
  • централизовать работу с временными зонами;
  • избежать неоднозначностей при парсинге.

Особенности сериализации и API

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

Наиболее стабильные варианты:

  • 2026-05-10T10:00:00Z
  • 1715335200000 (timestamp)

Joi в этих случаях выполняет роль фильтра, но не интерпретатора бизнес-смысла.


Поведение при невалидных временных значениях

Если строка не может быть интерпретирована как дата:

Joi.date().validate("invalid-date");

Результат:

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

При этом временная зона не влияет на сам факт валидности строки — важен только общий формат и возможность парсинга.


Итоговые особенности работы временных зон в Joi

Работа с временными зонами в Joi сводится к следующим техническим ограничениям:

  • Joi не хранит временную зону как отдельную сущность;
  • Joi опирается на поведение JavaScript Date;
  • ISO 8601 является основным поддерживаемым форматом;
  • UTC используется как внутреннее представление;
  • сложная логика временных зон выносится за пределы Joi.