Artykuł

freeimages.com freeimages.com
wrz 17 2014
0

Czas w .Net cz. 1 - DateTime

W .Necie programuje już ponad od 5 lat, ale tak naprawdę nigdy dobrze nie zgłębiłem tematów związanych z czasem. Przez większość mojej programistycznej kariery wystarczała mi bazowa funkcjonalność, którą dostarcza klasa DateTime. Nie miałem po prostu większej styczności z projektami, które funkcjonowały by w różnych strefach czasowych.

Ostatnimi czasy postanowiłem jednak zmienić coś w tym temacie i w tzw. czasie wolnym od pracy, zacząłem tworzyć projekt, w którym użytkownik z założenia wybierałby swoją strefę czasową. Szybko okazało się, że z samą klasą DateTime daleko tu nie zajdę...

Podstawowe klasy związane z czasem w .Net

W .Net istnieje wiele klas które opisują czas, bądź też w jakikolwiek sposób wspomagają zarządzaniem nim. Tą najbardziej podstawową jest właśnie tytułowy DateTime. Oprócz niego warto również zwrócić uwagę na DateTimeOffset i TimeZoneInfo (tymi dwoma zajmiemy się w kolejnym wpisie). Te dwie ostatnie zostały wprowadzone dopiero w .Net Framework 3.5. Przedtem było więc naprawdę biednie. Zaraz dowiecie się czemu;-)

DateTime - podstawowa charakterystyka

DateTime to tak jak już wspomniałem wcześniej, najbardziej podstawowa klasa w .Net odpowiedzialna za przechowywanie informacji o dacie i czasie. Jest to stosunkowo dobre rozwiązanie w momencie gdy nasza aplikacja działa na pewnym określonym rynku, lub gdy działa lokalnie na komputerze. Przyczyna tego stanu rzeczy jest prosta - DateTime nie przenosi informacji o strefie czasowej, aczkolwiek częściowo radzi sobie z DST poprawnie wyznaczając czas UTC na podstawie daty lokalnej (i odwrotnie).

Obiekt klasy DateTime możemy stworzyć na kilka sposobów:

  • Wykorzystując konstruktor klasy DateTime
    DateTime d1 = new DateTime(2014, 09, 14);
    DateTime d2 = new DateTime(2014, 09, 14, 15, 0, 0);
    DateTime d3 = new DateTime(2014, 09, 14, 15, 0, 0, DateTimeKind.Utc);
  • Wykorzystując statyczne metody Parse/TryParse klasy DateTime
    DateTime dp = DateTime.Parse("2014-09-14 15:00:00");
  • Wykorzystując statyczną właściwość Now/UtcNow klasy DateTime
    DateTime nowLocal = DateTime.Now;
    DateTime nowUtc = DateTime.UtcNow;

Wszystkie powyższe sposoby tworzą obiekt DateTime, aczkolwiek każdy tego typu nowo utworzony obiekt, może mieć inny rodzaj.

DateTime.Kind - czyli określamy rodzaj daty

Każdy obiekt klasy DateTime ma pewien określony rodzaj, który w pewnym sensie determinuje w jaki sposób ma się on zachowywać - chodzi tu o punkt odniesienia. Rodzaj obiektu określamy za pomocą właściwości Kind, która może przyjąć następujące wartości:

  • UTC - obiekt DateTime przechowuje datę UTC. Możliwa jest konwersja do czasu lokalnego
    DateTime dateTimeU = new DateTime(2014, 09, 14, 15, 0, 0, DateTimeKind.Utc);
    // 2014-09-14 17:00:00
    Console.WriteLine(dateTimeU.ToLocalTime());
  • Local - obiekt DateTime przechowuje czas lokalny, czyli odnosi się do czasu komputera na którym została uruchomiona aplikacja
    DateTime dateTimeL = new DateTime(2014, 09, 14, 15, 0, 0, DateTimeKind.Local);
    // 2014-09-14 13:00:00
    Console.WriteLine(dateTimeL.ToUniversalTime());
  • Unspecified - domyślny rodzaj. Taka wartość jest ustawiana automatycznie w sytuacji gdy nie określiliśmy innego typu. Możemy tutaj dokonać konwersji do UTC jak i czasu lokalnego, aczkolwiek rezultaty w tym przypadku mogą być odrobinę nieprzewidywalne;-)
    DateTime dateTimeUns = new DateTime(2014, 09, 14, 15, 0, 0, DateTimeKind.Unspecified);
    // 2014-09-14 13:00:00
    Console.WriteLine(dateTimeUns.ToUniversalTime());
    // 2014-09-14 17:00:00
    Console.WriteLine(dateTimeUns.ToLocalTime());

Nie muszę chyba mówić, że powinniśmy dążyć przede wszystkim do pierwszej i drugiej opcji zależnie to od kontekstu;-)

Now vs UtcNow - UWAGA!

Statyczna metoda Now to prawdopodobnie jedna z najpopularniejszych metod klasy DateTime. Generalnie tworzy ona nowy obiekt uwzględniający czas lokalny, który od razu ma określony rodzaj jako Local. W praktyce więc, jest to bardzo przydatna metoda, aczkolwiek jej użycie niesie spore ryzyko.

Wyobraźmy sobie prosty przypadek. Tworzymy usługę która uruchamia się co określony interwał czasu i realizuje pewne zadania, a na końcu oblicza czas pracy. Nieświadomy zagrożeń programista napisał następujący kod:

DateTime now1 = DateTime.Now;
// Jakieś operacje
DateTime now2 = DateTime.Now;
TimeSpan ts = now2 - now1;

I generalnie wszystko byłoby w porządku, gdyby nie czas letni. Przychodzi więc noc 26 października i zegar nagle przesuwa się z godziny 3 na 2. Pierwszy obiekt DateTime zostały utworzony jeszcze w starym czasie, natomiast drugi w nowym. Nietrudno sobie wyobrazić konsekwencje... Czy można uniknąć tego problemu? W tym przypadku jak najbardziej. Wystarczy tylko zastosować statyczną metodę UtcNow, która nie ma problemów z DST, a w tym przypadku przecież tak naprawdę nie interesuje nas godzina, a różnica w operacji na dwóch datach:

DateTime utcNow1 = DateTime.UtcNow;
// Jakieś operacje 
DateTime utcNow2 = DateTime.UtcNow;
TimeSpan utcTs = utcNow2 - utcNow1;

Ten kod zadziała zawsze tak jak trzeba. Czy zatem klasyczna metoda Now jest bez sensu? Nie do końca, wciąż dobrze sprawdzi się ona w wielu miejscach gdzie operujemy na czasie lokalnym i nie musimy kalkulować dat.

Formatowanie

Gdy mamy już gotowy obiekt klasy DateTime, wypadałoby by go ostatecznie wyświetlić użytkownikowi. W tym celu warto wykorzystać jedną z predefiniowanych metod (lista poniżej), bądź też wykorzystać metodę ToString ze wskazanym przez nas formatem. UWAGA - poniższe metody są wrażliwe na ustawienia regionalne (Culture):

DateTime dateTimeL = new DateTime(2014, 09, 14, 15, 0, 0, DateTimeKind.Local);
// 14 września 2014
Console.WriteLine(dateTimeL.ToLongDateString());
// 15:00:00
Console.WriteLine(dateTimeL.ToLongTimeString());
// 2014-09-14
Console.WriteLine(dateTimeL.ToShortDateString());
// 15:00
Console.WriteLine(dateTimeL.ToShortTimeString());

Ustawienia regionalne możemy nadpisać w standardowy sposób:

Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("culture-name");
            Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;

Więcej możliwości daje wspomniana wcześniej metoda ToString. Poniżej przykładowy format daty i godziny, który zadziała tak samo na całym świecie, niezależnie od ustawień regionalnych maszyny na której zostanie uruchomiony kod:

DateTime dateTimeL = new DateTime(2014, 09, 14, 15, 00, 00, DateTimeKind.Local);
// 2014-09-14 15:00:00
Console.WriteLine(dateTimeL.ToString("yyyy-MM-dd HH:mm:ss"));

Oczywiście z założenia chcielibyśmy by każdy użytkownik niezależnie od swojej lokalizacji, korzystał z ustawień domyślnych dla swojego kraju, dlatego też wyświetlanie dat za pomocą czterech predefiniowanych wyżej metod nie jest złym pomysłem. Jeśli to Was nie zadowala, to pod wskazanym linkiem znajdziecie więcej gotowych formatów;-)

Podsumowanie

DateTime jako podstawowa klasa do obsługi daty wydaje się być całkiem przyzwoitą konstrukcją. Obiekty tej klasy sprawdzą się szczególnie, gdy działamy lokalnie na komputerze użytkownika. Duży problem pojawi się jednak w momencie gdy będziemy korzystać z bazy danych, a nasza aplikacja będzie działać w różnych strefach czasowych - niestety te informacje tracimy w momencie gdy skorzystamy z pola typu DateTime w bazie danych. Są na to jednak pewne sposoby i można również skorzystać z dodatkowych klas. O tym napiszę jednak więcej w drugiej i trzeciej części cyklu.

Cykl

Data ostatniej modyfikacji: 04.10.2014, 15:05.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus