Asi jste zaznamenali, že nové telefony od Apple měli tento rok problém probudit své šťastné majitele po změně letního času. Přesnou příčinu nevím, ale protože práce s datem a časem není úplně triviální pokusím se zde shrnout některé zásady a ukázat, kam jejich porušení může vést.
Časová pásma
Na různých místech jsou různé časy – to je asi známé. Zatímco v Brně obědváme, v New Yorku se lidé teprve probouzejí a naopak v australském Perthu jsou lidé už pomalu po večeři. Ostatně si můžete i na svém desktopu umístit několik hodin ukazujících časy na různých místech. A nebo si můžete čas přepočítat na těchto stránkách.
Aby v tom byl pořádek, rozdělil se svět na časové zóny. Každá zóna má svůj název a definovanou odchylku od UTC (Universal Time Coordinate) – což je zjednodušeně řečeno čas na nultém poledníku, který prochází Greenwich (část Londýna). Česká republika tak leží v zóně s názvem Central Europe Standard Time s ochylkou +1:00 od UTC – což znamená, že pokud je UTC = 9 hodin, je u nás 10, tedy o hodinu více (proto to znaménko plus +). Naopak v Rio De Janeiro, které se nachází v pásmu –2:00 je 7 hodin, tedy o dvě hodiny méně (což je symbolizováno znamením mínus -).
Co je ještě důležité vědět: UTC bývá také nazýváno jako Zulu či GMT a odchylka nemusí být nutně v celých hodinách, ale je běžná i odchylka například o 5 hodin a 30 minut.
DST – ďáblův dar
DST je zkratka Daylight saving time, u nás známá jako letní čas. Prakticky se projevuje tak, že se na jaře vyspíte o hodinu méně a poté obíháte hodiny v bytě a posunujete ručičku dopředu, aby jste si totéž zopakovali na podzim s tím, že sílu na posunutí ručičky dozadu můžete načerpat během hodiny, o kterou se váš spánek prodlouží. Praktický smysl to nemá a programátorům to jen komplikuje život – protože ve skutečnosti jsme předefinovali chování časové zóny – pokud jsem v předchozí stati uvedl, že je u nás 10 hodin, byl správný zápis tohoto údaje 10:00 UTC+1:00. Ale jakmile přejdeme na DST, letní čas, máme sice i nadále 10 hodin, ale posuv vůči UTC jsou dvě hodiny a správný zápis je 10:00 UTC+2:00. A název zóny zní najednou ….
To ještě není to nejhorší – protože DST je výmysl politiků, není datum zavedení DST jednotné. Spojené státy tak přechází na DST o dva týdny dříve a časový rozdíl mezi NewYorkem a Prahou není tak šest hodin, ale jen pět.
Základní .NET třídy-struktury pro práci s časem
V prostředí .NET jsou k dispozici třídy DateTime a TimeZone. DateTime slouží k vyjádření času, který může být jednoho z těchto typů":
- local
- universal
- unspecified
Local znamená, že hodnota vyjadřuje čas v zóně, ve které se nachází počítač. Universal je pak čas UTC. Unspecified označuje jakýkoliv čas mimo dva předešlé. Pro práci platí tyto hlavní zásady:
- pracujeme (=provádíme operace) vždy s univerzálním časem
- lokální čas používáme jen při zpracování vstupu od uživatele a při prezentaci výsledku uživateli
Porušení těchto zásad vede v zásadě k chybnému programu. Pokusím se to ukázat na následujícím příkladu, kdy program počítá vždy čas za hodinu:
DateTime currentTime = new DateTime(2011, 3, 27, 1, 30, 0, 0, DateTimeKind.Local);
DateTime futureTime = currentTime.AddHours(1);
Console.WriteLine("{0:o} - {1:o}", currentTime, futureTime);
A co dostaneme:
Tento výsledek je špatně – 27.3.2011 přecházíme na letní čas a 2:30 tedy vůbec nebude. Správně by se všechny výpočty měli provádět v UTC:
DateTime currentTime = new DateTime(2011, 3, 27, 1, 30, 0, 0, DateTimeKind.Local);
DateTime futureTime = currentTime.ToUniversalTime().AddHours(1).ToLocalTime();
Console.WriteLine("{0:o} - {1:o}", currentTime, futureTime);
a poté již dostaneme správný výsledek:
Třída TimaZone nám dokáže zejména vracet údaje pro časovou zónu počítače, na kterém běží naše aplikace. Její praktické použití je tak omezené.
Ukládáme čas
Ukládání časových údajů se řídí zásadami, které vyplývají z hlavních zásad:
- ukládáme vždy čas v UTC
- ukládáme údaj identifikující časovou zónu uživatele
Představme si, že jsme si uložili čas jako řetězec a to pro účely buzení uživatele. Čas jsme ukládali v létě a uložili jsme si tuto hodnotu a uložili jsme si údaj v lokálním čase uživatele a uložili jsme si ho včetně posunu – prostě tak, jako kdybychom plně nepochopili význam výše uvedených pravidel. Poté se pokusíme každý den určit, kdy uživatele probudit.
DateTime wakeUpTime = DateTime.Parse("6:00+2:00");
Console.WriteLine("{0:o}", wakeUpTime);
a program funguje, pěkne nás vzbudí:
Jenže pak se změní čas a doposud bezchybný program nás začne budit o hodiny dříve:
Takže takto bychom čas ukládat určitě neměli – vždy ho ukládáme nejlépe v UTC a s informací o zóně, kde má původ. Časový posun vůči UTC tímto údajem rozhodně není, neboť se může díky DST změnit.
Globalizujeme
s výše uvedenými věcmi nešlo vystačit. Programátoři se mohli setkat například s požadavkem, aby na počítači v Praze zobrazili mimo místního pražského času i čas v Los Angeles. Tento úkol není tak triviální jak se může zdát – někteří jej řešili tak, že od místního času odečetli devět hodin (Praha je v zoně UTC+1:00, Los Angeles v zoně UTC-8:00, rozdíl devět hodin a stačí tedy napsat:
DateTime currentTime = DateTime.Now;
DateTime losAngelesTime = currentTime.AddHours(-9);
Console.WriteLine("{0:o} - {1:o}", currentTime, losAngelesTime);
Program nám vypíše:
Na první pohled vypadá vše v pořádku. Můžeme si ale všimnout, že čas Los Angeles má chybně vyjádřený posuv. A celý příklad obsahuje ještě jednu chybu - nebere v úvahu DST. Tři týdny v roce bude tedy přepočet chybný.
Uvádění časového posunu se můžeme samozřejmě vyhnout, ale DST chybu odstraníme stěží. Naštěstí máme v .NET 3.5 více možností – je zde nová struktura DateTimeOffset a nová třída pro práci s časovými zónami TimeZoneInfo.
DateTimeOffset
je vlastně rozšířením DateTime, ale mim datumu a času drží i informaci o posunu vůči UTC. Jinak se ničím neliší a při práci s ní je nutno dodržovat veškeré zásady práce s DateTime.
Následující příklad sice vyřeší správné zobrazení, ale pořád je tam skrytá chyba při přechodu na letní čas a naopak:
DateTime currentTime = DateTime.Now;
DateTime losAngelesTime = DateTime.SpecifyKind(currentTime.AddHours(-9), DateTimeKind.Unspecified);
DateTimeOffset formattedLosAngelesTime = new DateTimeOffset(losAngelesTime, new TimeSpan( -8, 0, 0));
Console.WriteLine("fff{0:o} - {1:o}", currentTime, formattedLosAngelesTime);
Výpis bude obsahovat správné časové posuny vůči UTC:
Ale pozor, jak se můžeme přesvědčit, budou tyto údaje zobrazovány i v okamžiku, kdy je časová zóna v Los Angeles stále v zóně -7:00
Proto existuje třída TimeZoneInfo.
TimeZoneInfo
Pomocí této třídy si můžeme vytvořit objekt pro danou časovou zónu a s jeho pomocí vytvářet časy, které jsou platné v této časové zóně. Navíc nám statická metoda této třídy zpřístupňuje seznam všech časových zón tak, jak jsou definovány v OS a udržovány pomocí aktualizací Microsoftem. Měli bychom tak mít zaručenou aktualizaci pokud dojde ke změně.
TimeZoneInfo pacificTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
DateTime currentUtcTime = DateTime.UtcNow;
DateTime currentLosAngelesTime = TimeZoneInfo.ConvertTimeFromUtc(currentUtcTime, pacificTimeZone);
Console.WriteLine("{0:o}", new DateTimeOffset(currentLosAngelesTime, pacificTimeZone.GetUtcOffset(currentLosAngelesTime)));
Jak během normálního, tak i zimního času dostaneme správný tvar, během letního (DST):
tak i v zimním období:
Objekty třídy TimeZoneInfo nám zpřístupňují mnoho informací o dané časové zóně. řeknou nám, jestli daný čas v dané zoně vůbec existuje (v případě, že se přechází ze zimního na letní čas, chybí nám celá hodina od 2 do 3 ráno) a nebo naopak jestli daný čas neexistuje dvakrát (pokud přecházíme z letního na zimní čas opakuje se v noci celá hodina od 2 do 3 ráno. Objekt nám pro dané datum a čas je schopen vrátit správnou odchylku od UTC. Některé možnosti jsou zachyceny v přechozí ukázce i následující ukázce:
TimeZoneInfo centralEuropeTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Central Europe Standard Time");
//31.10. 2010 - 2:30 AM
DateTime shiftSummerWinterTime = new DateTime(2010, 10, 31, 2, 30, 0, DateTimeKind.Unspecified);
//27.3. 2011 - 2:30 AM
DateTime shiftWinterSummerTime = new DateTime(2011, 3, 27, 2, 30, 0, DateTimeKind.Unspecified);
Console.WriteLine("Summer to winter - two possibilities? {0}", centralEuropeTimeZone.IsAmbiguousTime(shiftSummerWinterTime)); // true
Console.WriteLine("Summer to winter - exists? {0}", !centralEuropeTimeZone.IsInvalidTime(shiftSummerWinterTime)); //true
Console.WriteLine("Winter to summer - two possibilities? {0}", centralEuropeTimeZone.IsAmbiguousTime(shiftWinterSummerTime)); //false
Console.WriteLine("Winter to summer - exists?{0}", !centralEuropeTimeZone.IsInvalidTime(shiftWinterSummerTime)); //false
foreach (var zone in TimeZoneInfo.GetSystemTimeZones())
{
Console.WriteLine("{0} {1} {2} {3} {4} {5}",
zone.Id, //Central Europe Standard Time
zone.StandardName, //Central Europe Standard Time
zone.DisplayName, //(UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague
zone.DaylightName, //Central Europe Daylight Time
zone.BaseUtcOffset, //01:00:00
zone.SupportsDaylightSavingTime //true
);
}
A nakonec ještě stručné schéma jak správně pracovat s časy a datumy:
Poznámka: ukázky v tomto příspěvku byly spouštěny na PC ve středoevropském časovém pásmu, kde 31.10.2010 došlo k přechodu na zimní čas a 27.3. 2011 dojde k přechodu na letní čas.