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 трапляється двічі — і обидва рази «різні». - Не забувайте: список часових поясів може змінюватися з оновленнями ОС! Наприклад, деякі країни скасовують або запроваджують переходи на літній час.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ