uchill/front_material/utils/timezone.ts

202 lines
8.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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