202 lines
7.8 KiB
TypeScript
202 lines
7.8 KiB
TypeScript
/**
|
||
* Утилиты для работы с часовыми поясами.
|
||
*
|
||
* Поддерживаемые форматы 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;
|
||
}
|
||
}
|