/** * Утилиты для работы с часовыми поясами. * * Поддерживаемые форматы timezone: * - UTC+X, UTC-X (например, "UTC+8", "UTC-5") * - GMT+X, GMT-X * - IANA названия (например, "Europe/Moscow", "Asia/Irkutsk") */ /** * Парсить timezone и получить смещение в минутах. * * Примеры: * - "UTC+8" -> 480 (8 * 60) * - "UTC-5" -> -300 (-5 * 60) * - "UTC+5:30" -> 330 (5 * 60 + 30) * * @returns смещение в минутах или null если не удалось распарсить */ export function parseTimezoneOffset(timezone: string | undefined): number | null { if (!timezone) return null; const trimmed = timezone.trim(); // Парсим формат UTC+X, UTC-X, GMT+X, GMT-X const match = trimmed.match(/^(?:UTC|GMT)([+-])(\d{1,2})(?::(\d{2}))?$/i); if (match) { const sign = match[1] === '+' ? 1 : -1; const hours = parseInt(match[2], 10); const minutes = match[3] ? parseInt(match[3], 10) : 0; return sign * (hours * 60 + minutes); } return null; } /** * Получить смещение часового пояса в минутах. * * Поддерживает: * - UTC+X формат (парсит напрямую) * - IANA названия (использует Intl API) * * @returns смещение в минутах (положительное = восток от UTC) */ export function getTimezoneOffsetMinutes(timezone: string | undefined): number { if (!timezone) { // Браузерный timezone return -new Date().getTimezoneOffset(); } // Сначала пробуем распарсить UTC+X формат const parsedOffset = parseTimezoneOffset(timezone); if (parsedOffset !== null) { return parsedOffset; } // Для IANA названий используем Intl API try { const now = new Date(); const utcDate = new Date(now.toLocaleString('en-US', { timeZone: 'UTC' })); const tzDate = new Date(now.toLocaleString('en-US', { timeZone: timezone })); return Math.round((tzDate.getTime() - utcDate.getTime()) / 60000); } catch { // Fallback на браузерный timezone return -new Date().getTimezoneOffset(); } } /** * Создать ISO строку даты/времени с учетом часового пояса пользователя. * * Пример: Если пользователь в Улан-Удэ (UTC+8) вводит 18:00, * то нужно отправить на сервер 10:00 UTC (18:00 - 8 часов). * * @param dateStr - дата в формате 'YYYY-MM-DD' * @param timeStr - время в формате 'HH:mm' * @param userTimezone - часовой пояс пользователя (например, 'UTC+8', 'Europe/Moscow') * @returns ISO строка в UTC */ export function createDateTimeInUserTimezone( dateStr: string, timeStr: string, userTimezone: string | undefined ): string { // Парсим дату и время const [year, month, day] = dateStr.split('-').map(Number); const [hours, minutes] = timeStr.split(':').map(Number); // Создаем дату как будто она в UTC const utcDate = new Date(Date.UTC(year, month - 1, day, hours, minutes, 0, 0)); // Получаем смещение timezone пользователя const offsetMinutes = getTimezoneOffsetMinutes(userTimezone); // Корректируем: вычитаем смещение, чтобы получить UTC // Например: 18:00 в UTC+8 = 10:00 UTC, значит вычитаем 8 часов (480 минут) utcDate.setMinutes(utcDate.getMinutes() - offsetMinutes); return utcDate.toISOString(); } /** * Парсить ISO дату и получить локальную дату/время в часовом поясе пользователя. * * Работает с любым форматом timezone: * - UTC+8: добавляет 8 часов к UTC * - Europe/Moscow: использует Intl API * * @param isoString - ISO строка даты (например, '2026-02-21T10:00:00Z' для UTC) * @param userTimezone - часовой пояс пользователя (например, 'UTC+8') * @returns объект с date и time в часовом поясе пользователя */ export function parseISOToUserTimezone( isoString: string, userTimezone: string | undefined ): { date: string; time: string; dateObj: Date } { // Парсим ISO строку в UTC timestamp const utcDate = new Date(isoString); const utcMs = utcDate.getTime(); // Получаем смещение timezone пользователя в минутах const offsetMinutes = getTimezoneOffsetMinutes(userTimezone); // Применяем смещение: UTC + offset = локальное время // Например: 10:00 UTC + 8 часов = 18:00 в UTC+8 const localMs = utcMs + offsetMinutes * 60 * 1000; const localDate = new Date(localMs); // Извлекаем компоненты даты/времени в UTC (потому что мы уже добавили offset) const year = localDate.getUTCFullYear(); const month = String(localDate.getUTCMonth() + 1).padStart(2, '0'); const day = String(localDate.getUTCDate()).padStart(2, '0'); const hours = String(localDate.getUTCHours()).padStart(2, '0'); const minutes = String(localDate.getUTCMinutes()).padStart(2, '0'); const dateStr = `${year}-${month}-${day}`; const timeStr = `${hours}:${minutes}`; // Создаем Date объект для использования в UI (в локальном времени браузера) const displayDate = new Date(`${dateStr}T${timeStr}`); return { date: dateStr, time: timeStr, dateObj: displayDate, }; } /** * Форматировать дату для отображения в часовом поясе пользователя. * * @param isoString - ISO строка даты * @param userTimezone - часовой пояс пользователя (например, 'UTC+8') * @param options - опции форматирования Intl.DateTimeFormat */ export function formatDateInUserTimezone( isoString: string, userTimezone: string | undefined, options: Intl.DateTimeFormatOptions = {} ): string { // Получаем локальное время в timezone пользователя const parsed = parseISOToUserTimezone(isoString, userTimezone); // Форматируем используя Intl (dateObj уже в правильном времени) return new Intl.DateTimeFormat('ru-RU', options).format(parsed.dateObj); } /** * Получить текущую дату/время в часовом поясе пользователя. */ export function getNowInUserTimezone(userTimezone: string | undefined): Date { const now = new Date(); const parsed = parseISOToUserTimezone(now.toISOString(), userTimezone); return parsed.dateObj; } /** * Получить название часового пояса с offset. * Например: 'Europe/Moscow' -> 'Europe/Moscow (UTC+3)' * Для 'UTC+8' -> 'UTC+8' */ export function getTimezoneDisplayName(timezone: string): string { if (!timezone) return ''; // Если уже в формате UTC+X, возвращаем как есть if (/^(?:UTC|GMT)[+-]\d/i.test(timezone)) { return timezone; } try { const offsetMinutes = getTimezoneOffsetMinutes(timezone); const hours = Math.floor(Math.abs(offsetMinutes) / 60); const mins = Math.abs(offsetMinutes) % 60; const sign = offsetMinutes >= 0 ? '+' : '-'; const offsetStr = mins > 0 ? `${hours}:${mins.toString().padStart(2, '0')}` : `${hours}`; return `${timezone} (UTC${sign}${offsetStr})`; } catch { return timezone; } }