1. Введение
Если вы думаете, что таймзоны — это просто "прибавить/вычесть сколько-то часов", вы очень скоро почувствуете всю боль большинства программистов на планете. Часовые пояса, перевод часов на зимнее/летнее время, неожиданные исключения при попытке "перевести время в другой регион" — всё это может привести к багам, которые сложно найти и ещё сложнее объяснить пользователям.
Самое ужасное: в мире нет и никогда не было "универсального" понимания времени — например, две соседние страны могут по-разному относиться к переходу на летнее время, а иногда решения о переходе принимаются посреди года (да, это реально так!). Пример: в 2011 году Самоа "перепрыгнула" через целый день и сменила сторону относительно линии перемены дат.
Но C# и .NET дают мощные инструменты, чтобы хоть как-то с этим бороться. Сегодня мы познакомимся с нашим спасителем — типом TimeZoneInfo и его функциями, которые помогут вам и вашему приложению.
Зачем и когда вам нужна работа с таймзонами
- Когда пользователи/сервера находятся в разных регионах мира
- Когда вы храните дату и время в базе данных в UTC, а показываете пользователю в его местном времени
- Для расчёта расписаний, напоминаний, онлайн-событий ("Вебинар начнётся в 19:00 по Берлину — а в вашем регионе?")
- При разработке корпоративных/глобальных систем (например, бухгалтерия или CRM для международного бизнеса)
Обзор типа TimeZoneInfo
TimeZoneInfo — это специально созданный класс в .NET, предназначенный для представления информации о часовом поясе: его смещение относительно UTC, название, поддержка переходов на летнее время и т.д.
В отличие от устаревшего класса TimeZone, который знáет только про "местный" и "UTC" часовые пояса, TimeZoneInfo даёт полноценный доступ ко всем часовым поясам, зарегистрированным в системе (и даже к произвольным, созданным вручную).
2. Получение информации о таймзонах
Локальный и UTC пояса
Получить информацию о локальном часовом поясе очень легко:
// Локальный (т.е. где сейчас работает ваша программа)
TimeZoneInfo local = TimeZoneInfo.Local;
// Всегда UTC
TimeZoneInfo utc = TimeZoneInfo.Utc;
Console.WriteLine(local.DisplayName); // Например: (UTC+01:00) Berlin, Vienna
Console.WriteLine(utc.DisplayName); // (UTC) Coordinated Universal Time
Все доступные таймзоны
Часто нужно показать пользователю список всех возможных таймзон (например, при регистрации в сервисе):
foreach (var tz in TimeZoneInfo.GetSystemTimeZones())
{
Console.WriteLine($"{tz.Id} | {tz.DisplayName}");
}
Это выведет список типа:
- Central European Standard Time | (UTC+01:00) Berlin, Vienna
- Pacific Standard Time | (UTC-08:00) Pacific Time (US & Canada)
- и т.д.
Визуализация: основные свойства TimeZoneInfo
| Свойство | Описание |
|---|---|
|
Системный идентификатор таймзоны (используется при поиске) |
|
Описание для пользователя (с часами и городами) |
|
Название "обычного" времени |
|
Название времени для перехода на летнее время |
|
Смещение от UTC (например, +01:00 для Берлина) |
|
True, если таймзона поддерживает переходы на летнее/зимнее время |
3. Конвертация времени между таймзонами
Вот зачем всё это затевалось! Вам нужно:
- Перевести время из одной зоны в другую (например, сервер записал в UTC, а отображаем пользователю в его поясе)
- Правильно обработать перевод на летнее/зимнее время
Общий синтаксис
DateTime utcNow = DateTime.UtcNow;
// Например, пересчитаем это время в центральноевропейский часовой пояс
TimeZoneInfo europeZone = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time");
DateTime europeTime = TimeZoneInfo.ConvertTimeFromUtc(utcNow, europeZone);
Console.WriteLine($"UTC now: {utcNow}"); // 2024-06-20 10:30:00
Console.WriteLine($"Europe now: {europeTime}"); // 2024-06-20 11:30:00
Важно: Всегда используйте UTC для внутренних расчётов и хранения, и только "на выходе" переводите в нужную зону!
Как узнать идентификатор таймзоны?
В Windows идентификаторы (Id) таймзон специфичны (например, "Central European Standard Time"), а на Linux/Unix — обычно это IANA/Olson-идентификаторы ("Europe/Berlin", "America/New_York").
Список идентификаторов на вашей системе можно получить через TimeZoneInfo.GetSystemTimeZones().
4. Конвертация между произвольными зонами
Допустим, вы получили время в Лондоне (GMT/UTC+0) и хотите перевести его в Токио (UTC+9):
DateTime londonTime = new DateTime(2024, 6, 20, 12, 0, 0, DateTimeKind.Unspecified);
TimeZoneInfo londonZone = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time");
TimeZoneInfo tokyoZone = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
// Первый шаг: перевести время в UTC (если оно локальное)
DateTime utc = TimeZoneInfo.ConvertTimeToUtc(londonTime, londonZone);
// Второй шаг: перевести UTC в Токийское время
DateTime tokyoTime = TimeZoneInfo.ConvertTimeFromUtc(utc, tokyoZone);
Console.WriteLine($"London: {londonTime} | UTC: {utc} | Tokyo: {tokyoTime}");
Работа с переходами на летнее и зимнее время
TimeZoneInfo учитывает переходы на летнее/зимнее время — если нужная зона их поддерживает.
TimeZoneInfo eastern = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
DateTime dateWinter = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Unspecified);
DateTime dateSummer = new DateTime(2024, 7, 1, 12, 0, 0, DateTimeKind.Unspecified);
Console.WriteLine( TimeZoneInfo.ConvertTimeToUtc(dateWinter, eastern)); // Смещение UTC-5
Console.WriteLine( TimeZoneInfo.ConvertTimeToUtc(dateSummer, eastern)); // Смещение UTC-4 — летнее время!
Даже если США (из примера выше) откажутся от перехода на летнее время (а такие инициативы бывают каждый год!), база таймзон в вашей ОС всё равно будет обновляться, и ваш код будет работать корректно.
5. Практические советы и частые ошибки
Исключения и подводные камни
- На месте, где переводятся часы (например, 2:30 ночи в день перехода на летнее/зимнее время), некоторые времена могут быть "несуществующими" или "повторяющимися":
Например, в день перевода "назад" 2:30 происходит дважды — и оба раза "разные". - Не забывайте: список таймзон может меняться обновлениями ОС! Пример: некоторые страны отменяют/вводят переходы на летнее время.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ