Operowanie na datach i czasie bywa zaskakująco skomplikowane. Nawet doświadczeni programiści potrafią napotkać problemy wynikające ze stref czasowych czy zmian czasu letniego.
W języku C# podstawowym typem do reprezentowania daty i czasu jest struktura DateTime. W tym artykule wyjaśnię, jak poprawnie używać DateTime w C#, omówię najważniejsze zagadnienia związane z pracą z czasem (takie jak formatowanie dat, strefy czasowe, różnice między typami) oraz wskażę typowe pułapki, na które warto uważać.
Czym jest DateTime w C#?
DateTime to struktura (typ wartościowy) w .NET, która służy do przechowywania informacji o konkretnej dacie i czasie. Możemy za jej pomocą reprezentować zarówno datę (np. 22 października 2025), jak i czas (np. godzina 13:45:30), łącznie w jednym obiekcie. DateTime posiada wiele pomocnych właściwości i metod – np. pozwala odczytać rok, miesiąc, dzień, godzinę, minutę itp., a także wykonywać operacje arytmetyczne na datach (dodawanie dni, porównywanie dat, wyliczanie różnicy czasu itp.).
Warto pamiętać, że DateTime może również przechowywać informację o tzw. kind, czyli kontekście czasowym:
- Local – czas lokalny (w strefie czasowej ustawionej na danym komputerze),
- UTC – czas uniwersalny (Greenwich, strefa zero),
- Unspecified – brak określenia (domyślna wartość, gdy nie sprecyzujemy strefy).
Ta informacja jest istotna przy konwersjach między strefami czasowymi, o czym powiemy później.
Pobieranie bieżącej daty i czasu
Najczęściej potrzebujemy po prostu uzyskać aktualny moment – tu z pomocą przychodzi statyczna właściwość DateTime.Now. Zwraca ona bieżącą datę i czas zgodnie z czasem lokalnym komputera, na którym uruchomiono kod. Przykład użycia:
DateTime now = DateTime.Now;
Console.WriteLine(now);
// Przykładowy wynik: 22.10.2025 13:45:30Jeśli zależy nam na czasie UTC (uniwersalnym, niezależnym od lokalnej strefy czasowej), możemy użyć DateTime.UtcNow:
DateTime nowUtc = DateTime.UtcNow;
Console.WriteLine(nowUtc);
// Przykładowy wynik: 22.10.2025 11:45:30 (czas UTC może różnić się od lokalnego)Wskazówka: Gdy potrzebujesz tylko dzisiejszej daty bez konkretnej godziny, skorzystaj z DateTime.Today. Ta właściwość ustawia godzinę na 00:00:00 danego dnia (początek doby).
Formatowanie i parsowanie dat
Często zachodzi potrzeba wyświetlenia daty w określonym formacie (np. tylko data bez czasu, w formacie RRRR-MM-DD, czy z pełną nazwą miesiąca). DateTime udostępnia metodę .ToString(), która pozwala sformatować datę do napisu według podanego wzorca formatu.
Przykład formatowania:
DateTime date = new DateTime(2025, 10, 22, 13, 45, 30);
string format1 = data.ToString("yyyy-MM-dd"); /* "2025-10-22" */
string format2 = data.ToString("dd MMMM yyyy"); /* "22 października 2025" */
string format3 = data.ToString("HH:mm:ss"); /* "13:45:30" */
Console.WriteLine(format2);W powyższym przykładzie użyliśmy ciągów formatujących:
- yyyy – rok (czterocyfrowo),
- MM – miesiąc (dwucyfrowo),
- dd – dzień miesiąca,
- MMMM – pełna nazwa miesiąca (np. "października"),
- HH – godzina (00–23, format 24-godzinny),
- mm – minuty, ss – sekundy.
Istnieją także standardowe formaty (np. data.ToString("d") dla krótkiego formatu daty zależnego od kultury, czy data.ToString("g") dla standardowego formatu data+godzina). Więcej informacji o formatach znajdziesz w dokumentacji .NET, ale te kilka podstawowych powinno wystarczyć na początek.
Parsowanie to odwrotna operacja – zamiana napisu na obiekt DateTime. Służą do tego m.in. metody DateTime.Parse oraz bezpieczniejsza DateTime.TryParse. Przykład:
string textDate = "2025-10-22";
if (DateTime.TryParse(textDate, out DateTime dataParsed))
{
Console.WriteLine(dataParsed.ToString());
/* Wynik: 22.10.2025 00:00:00 */
}Metoda TryParse próbuje odczytać datę z tekstu i zwraca true/false w zależności od powodzenia operacji, a wynik (jeśli się uda) przekazuje w parametrze out. Należy uważać na format tekstu – powyższy przykład zadziała dla formatu ISO 8601 (RRRR-MM-DD). Jeśli tekst jest w innym formacie (np. "22/10/2025" albo "22 paź 2025"), należy użyć przeciążenia metody uwzględniającego kulturę (np. polską lub inną) lub skorzystać z DateTime.ParseExact, gdzie podamy dokładny oczekiwany format daty.
Czas uniwersalny a strefy czasowe
Różnice między strefami czasowymi to częste źródło problemów z datami. Czas lokalny (np. czas polski UTC+1 lub +2 w lecie) różni się w zależności od miejsca na świecie. Dlatego warto rozróżniać, kiedy używamy czasu lokalnego, a kiedy UTC.
Załóżmy, że nasza aplikacja działa na serwerze w Polsce, ale obsługuje użytkowników z różnych krajów. Jeśli zapiszemy czas zdarzenia jako lokalny (np. poprzez DateTime.Now), to użytkownik z innej strefy może zobaczyć nieprawidłową godzinę. Rozwiązanie: przechowuj czasy w formacie UTC, a do lokalnej strefy konwertuj je tylko podczas prezentacji dla użytkownika końcowego.
W C# konwersję czasu lokalnego na UTC wykonasz metodą ToUniversalTime(), a odwrotnie (UTC -> lokalny) metodą ToLocalTime(). Przykład:
DateTime local = DateTime.Now;
DateTime universal = local.ToUniversalTime();
DateTime backLocal = universal.ToLocalTime();
Console.WriteLine(local);
Console.WriteLine(universal);
Console.WriteLine(backLocal);Wypisany ponownie backLocal powinien odpowiadać oryginalnemu czasowi lokalnemu (pomijając ewentualną utratę precyzji ułamków sekund). Zwróć uwagę na właściwość Kind wspomnianą wcześniej – poprawna konwersja zależy od tego, czy DateTime wie, z jakim rodzajem czasu ma do czynienia. Przykładowo, jeśli obiekt DateTime ma Kind = Unspecified (nieznany), to wywołanie ToUniversalTime() założy domyślnie, że to czas lokalny, co może prowadzić do błędów w obliczeniach.
Dzień zmiany czasu (DST): W miejscach, gdzie obowiązuje zmiana czasu letni/zimowy, pewne godziny mogą nie istnieć lub wystąpić dwukrotnie. Np. w Polsce w ostatni weekend marca zegary przeskakują z 2:00 na 3:00 – godziny między 2:00 a 3:00 nie występują w lokalnym czasie. Odwrotnie jesienią, jeden godzinny przedział powtarza się dwukrotnie. Dlatego do obliczania przedziałów (np. różnicy między dwiema datami) bezpieczniej jest używać czasu UTC. W praktyce oznacza to, że mierząc czas trwania jakiegoś zdarzenia, lepiej pobrać czasy początkowy i końcowy za pomocą DateTime.UtcNow zamiast DateTime.Now. Dzięki temu unikniesz sytuacji, w której zmiana czasu zaburzy Twoje obliczenia.
DateTime vs DateTimeOffset vs inne typy czasu
W bibliotece .NET istnieje także struktura DateTimeOffset, która przechowuje nie tylko datę i czas, ale również offset od UTC (różnicę względem czasu uniwersalnego dla danej strefy). Innymi słowy, DateTimeOffset pozwala zapisać dokładny moment w czasie wraz z informacją, w jakiej strefie czasowej ten czas obowiązuje. Przykładowo zapis "2025-10-22 13:45:00 +02:00" jednoznacznie wskazuje, że jest to 13:45 w strefie UTC+2, co odpowiada 11:45 UTC.
Kiedy używać DateTimeOffset?
- Gdy zapisujesz daty i godziny, które powinny jednoznacznie wskazywać ten sam punkt czasowy na świecie, niezależnie od strefy. Przykładowo logi aplikacji lub znaczniki czasu zdarzeń – tutaj warto używać DateTimeOffset.UtcNow lub DateTime.UtcNow (a następnie przechowywać informację o strefie/offsetcie osobno lub zawsze interpretować zapis jako UTC).
- Gdy planujesz przyszłe lokalne wydarzenia (np. spotkanie za pół roku w konkretnej strefie czasowej) – wtedy lepiej przechować czas lokalny wraz z offsetem. Umożliwi to łatwe przeliczenie go na inne strefy w razie potrzeby, bez utraty oryginalnego kontekstu.
W .NET 6 dodano także typy DateOnly i TimeOnly. Jak sugerują nazwy, służą one odpowiednio do przechowywania wyłącznie daty (bez godziny) oraz wyłącznie czasu (bez kontekstu daty). Przydają się np. do reprezentowania dat urodzenia (gdzie interesuje nas dzień i miesiąc, ale nie godzina) albo godzin otwarcia sklepu (czas niezależny od konkretnej daty). Jeśli jednak pracujesz z pełnymi znacznikami czasu zawierającymi i datę, i godzinę, nadal będziesz używać głównie DateTime lub DateTimeOffset.
Typowe pułapki i najlepsze praktyki
Na koniec przyjrzyjmy się kilku typowym problemom, na jakie natrafiają programiści przy pracy z datami, oraz sposobom ich uniknięcia:
• Mieszanie czasu lokalnego z UTC: Upewnij się, że wiesz, w jakim formacie operujesz. Jeśli porównujesz dwie daty, obie powinny być w tej samej strefie (najlepiej obie w UTC). Staraj się przechowywać czasy zdarzeń w UTC lub używaj DateTimeOffset zamiast lokalnego DateTime, aby zachować kontekst strefy czasowej.
• Błędne formaty dat: Błędy przy parsowaniu często wynikają z różnic formatów (np. amerykański MM/dd/yyyy vs polski dd.MM.yyyy). Stosuj jednoznaczne formaty (np. ISO 8601 yyyy-MM-ddTHH:mm:ss przy komunikacji między systemami) lub jawnie podawaj kulturę/format w metodach parsujących i formatujących.
• Zmiany czasu (DST) a obliczenia: Jak wspomniałem wyżej, zmiana czasu potrafi zaburzyć obliczenia różnic. Unikaj wyliczania odstępów na podstawie lokalnego DateTime.Now. Zamiast tego do obliczeń czasu trwania używaj UTC (np. DateTime.UtcNow) lub odpowiednich struktur i klas (np. TimeSpan do przechowywania długości, DateTimeOffset do reprezentacji momentów w różnych strefach, klasa TimeZoneInfo do konwersji między strefami).
• Porównywanie tylko daty bez czasu: Jeżeli chcesz porównać dwie daty ignorując czas (np. sprawdzić, czy dziś to ten sam dzień co wczoraj, niezależnie od godziny), użyj właściwości DateTime.Date lub typu DateOnly. Bez tego porównanie dwóch obiektów DateTime prawie zawsze zwróci fałsz ze względu na różnicę w godzinach. Przykład: DateTime.Now.Date == DateTime.Today zwróci true, ponieważ oba wskazują północ tego samego dnia.
Stosując się do powyższych wskazówek, zredukujesz ryzyko błędów związanych z datami i czasem w swojej aplikacji.
Podsumowanie
Podsumowując, praca z datami i czasem w C# może wydawać się trudna, ale zrozumienie podstawowych zasad bardzo ułatwia życie programiście. Pamiętaj o różnicy między czasem lokalnym a UTC, naucz się formatować i parsować daty oraz poznaj narzędzia oferowane przez .NET (takie jak DateTimeOffset czy TimeZoneInfo). Dzięki temu Twoje aplikacje będą poprawnie działać niezależnie od strefy czasowej i ustawień regionalnych.
Na koniec pamiętaj, że nauka programowania to proces. Opanowanie pracy z DateTime to tylko jeden z kroków na tej drodze. Jeśli chcesz dalej rozwijać swoje umiejętności C#/.NET od podstaw i dążyć do pierwszej pracy jako programista, rozważ dołączenie do mojego kompletnego szkolenia online "Zostań Programistą .NET". Jest to kompleksowy, 15-modułowy program (około 3 miesiące nauki). Krok po kroku poprowadzi Cię on od absolutnych podstaw C# aż do poziomu, który pozwoli Ci ubiegać się o pierwszą pracę jako młodszy programista .NET. Niezależnie od tego, jaką ścieżkę nauki wybierzesz – powodzenia w zgłębianiu tajników .NET i pracy z datami.