Daty i czas — DateTime, TimeSpan i DateOnly
Ile dni zostało do zwrotu książki? Jaki to dzień tygodnia? Co 30 dni po 20 kwietnia? Przy dacie zapisanej jako napis każde z tych pytań to osobny algorytm. C# ma na to gotowy typ — razem z kilkoma pułapkami, które warto poznać od razu.
Czego się dziś nauczysz
- Wyjaśnisz, dlaczego data zapisana jako napis albo jako trzy liczby jest złym rozwiązaniem.
- Utworzysz
DateTimez zegara i z podanych składników oraz odczytasz z niego dzień tygodnia i numer dnia roku. - Wyjaśnisz, dlaczego
data.AddDays(30);bez przypisania nic nie zmienia. - Policzysz odstęp między dwiema datami i odróżnisz
DaysodTotalDays. - Złożysz plan dnia z odstępów
TimeSpani dodasz je do godziny. - Sformatujesz datę dla człowieka i dla pliku oraz wskażesz różnicę między
MMimm. - Wczytasz datę z tekstu przez
TryParseiTryParseExacti uzasadnisz wybór. - Wybierzesz między
DateTime,DateOnly,TimeOnlyiTimeSpan. - Policzysz wiek w pełnych latach i wyjaśnisz, dlaczego odjęcie lat nie wystarcza.
Przygotowanie: lekcje 01–58. Przewidywany czas: 90 min z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.
Data jako napis i sześć pytań bez odpowiedzi
Biblioteka szkolna zapisuje wypożyczenia. Każde ma datę i termin zwrotu. Pierwszy pomysł to napis:
class Wypozyczenie
{
public string Tytul { get; set; } = "";
public string Data { get; set; } = ""; // "12.05.2026"
}
Program się kompiluje i wszystko wygląda dobrze — dopóki nie przyjdzie pierwsze prawdziwe pytanie. A przyjdzie natychmiast:
| Pytanie | Przy dacie jako napis |
|---|---|
| Ile dni zostało do zwrotu? | trzeba rozbić napis, zamienić na liczby i policzyć — samemu obsługując lata przestępne |
| Które wypożyczenie jest najstarsze? | sortowanie alfabetyczne ustawi 01.01.2027 przed 12.05.2026 |
| Jaki to dzień tygodnia? | bez gotowego algorytmu — niewykonalne |
| Co 30 dni po 20 kwietnia? | trzeba znać długość każdego miesiąca |
Czy 31.02.2026 to poprawna data? | napis przyjmie wszystko |
Czy 12.05.2026 i 12.5.2026 to to samo? | dla napisu — nie |
Drugi pomysł to trzy liczby — Rok, Miesiac, Dzien. Sortowanie wtedy działa, ale wszystkie pozostałe problemy zostają, a dochodzi nowy: nic nie pilnuje, żeby te trzy liczby razem tworzyły istniejącą datę.
C# ma na to gotowy typ. Wszystkie sześć pytań z tabeli to jedna linia każde:
DateTime data = new DateTime(2026, 4, 20);
DateTime termin = data.AddDays(30); // 20.05.2026 - sam liczy miesiace
Console.WriteLine(termin);
Console.WriteLine(termin.DayOfWeek); // Wednesday
Console.WriteLine((termin - data).Days); // 30
Console.WriteLine(DateTime.IsLeapYear(2026)); // False
// DateTime zla = new DateTime(2026, 2, 30); // WYJATEK - nie ma 30 lutego
Trzy typy, jedno zadanie
W tej lekcji poznasz DateTime (chwila: data i godzina razem), TimeSpan (odstęp czasu, na przykład 45 minut albo 30 dni) oraz DateOnly i TimeOnly (osobno sama data i sama godzina, od .NET 6). Do tego dochodzi jedna rzecz, która psuje więcej programów niż wszystkie pozostałe razem: zapis daty w tekście.
Tworzenie i odczytywanie składników
DateTime przechowuje datę i godzinę w jednej wartości. Nawet gdy godzina Cię nie interesuje, ona tam jest — ustawiona na północ.
Pięć sposobów utworzenia
DateTime teraz = DateTime.Now; // data i godzina z zegara
DateTime dzis = DateTime.Today; // dzisiejsza data, godzina 00:00
DateTime terazUtc = DateTime.UtcNow; // czas uniwersalny (bez polskiej strefy)
DateTime dzienSlubu = new DateTime(2026, 5, 12); // 12.05.2026 00:00
DateTime spotkanie = new DateTime(2026, 5, 12, 14, 30, 0); // 12.05.2026 14:30:00
Console.WriteLine(DateTime.MinValue); // 01.01.0001 00:00:00
Console.WriteLine(DateTime.MaxValue); // 31.12.9999 23:59:59
Kolejność argumentów jest zawsze od największego
new DateTime(rok, miesiąc, dzień, godzina, minuta, sekunda) — od największej jednostki do najmniejszej. To nie jest polski zapis 12.05.2026 ani amerykański 05/12/2026. Zamiana roku z dniem kończy się wyjątkiem ArgumentOutOfRangeException, bo nie ma roku 12 i 2026 dnia miesiąca — i akurat w tym przypadku to dobra wiadomość.
Co można z daty odczytać
DateTime d = new DateTime(2026, 5, 12, 14, 30, 45);
Console.WriteLine(d.Year); // 2026
Console.WriteLine(d.Month); // 5
Console.WriteLine(d.Day); // 12
Console.WriteLine(d.Hour); // 14
Console.WriteLine(d.Minute); // 30
Console.WriteLine(d.Second); // 45
Console.WriteLine(d.DayOfWeek); // Tuesday - typ wyliczeniowy (lekcja 49)
Console.WriteLine(d.DayOfYear); // 132 - ktory to dzien roku
Console.WriteLine(d.Date); // 12.05.2026 00:00:00 - sama data
Console.WriteLine(d.TimeOfDay); // 14:30:45 - sama godzina jako TimeSpan
DayOfWeek zwraca wartość typu wyliczeniowego, więc da się jej użyć w switch tak samo jak każdego enum z lekcji 49:
static bool CzyWeekend(DateTime d)
{
return d.DayOfWeek == DayOfWeek.Saturday || d.DayOfWeek == DayOfWeek.Sunday;
}
static string Plan(DateTime d)
{
return d.DayOfWeek switch
{
DayOfWeek.Monday => "8 lekcji, pracownia 12",
DayOfWeek.Friday => "6 lekcji, krotszy dzien",
DayOfWeek.Saturday => "wolne",
DayOfWeek.Sunday => "wolne",
_ => "zwykly dzien"
};
}
Uwaga na pierwszy dzień tygodnia
DayOfWeek.Sunday ma wartość liczbową 0, a Monday — 1. W polskim kalendarzu tydzień zaczyna się w poniedziałek, więc przy rysowaniu kalendarza albo liczeniu numeru tygodnia nie rzutuj DayOfWeek na int bez przeliczenia. Wzór na „który to dzień tygodnia licząc od poniedziałku”: ((int)d.DayOfWeek + 6) % 7 — daje 0 dla poniedziałku i 6 dla niedzieli.
Dwie przydatne metody statyczne
Console.WriteLine(DateTime.DaysInMonth(2026, 2)); // 28
Console.WriteLine(DateTime.DaysInMonth(2024, 2)); // 29
Console.WriteLine(DateTime.IsLeapYear(2026)); // False
Console.WriteLine(DateTime.IsLeapYear(2024)); // True
// ostatni dzien biezacego miesiaca:
DateTime dzis = new DateTime(2026, 5, 12);
DateTime ostatni = new DateTime(dzis.Year, dzis.Month,
DateTime.DaysInMonth(dzis.Year, dzis.Month));
Console.WriteLine(ostatni); // 31.05.2026 00:00:00
Reguła lat przestępnych nie jest „co cztery lata” — rok podzielny przez 100 nie jest przestępny, chyba że dzieli się przez 400. Rok 1900 nie był przestępny, 2000 był. IsLeapYear zna tę regułę, a Twój własny warunek rok % 4 == 0 — nie.
Niezmienność, TimeSpan i porównywanie
Tu jest najważniejsza rzecz w całej lekcji, a wynika wprost z lekcji 50: DateTime jest niezmienną strukturą. Żadna metoda nie zmienia istniejącej daty — każda zwraca nową.
DateTime data = new DateTime(2026, 4, 20);
data.AddDays(30); // wynik wyrzucony - data sie NIE ZMIENILA
Console.WriteLine(data); // 20.04.2026
data = data.AddDays(30); // tak jest dobrze
Console.WriteLine(data); // 20.05.2026
Najczęstszy błąd z datami
data.AddDays(30); bez przypisania kompiluje się bez błędu i nic nie robi. To dokładnie ta sama pułapka co tekst.ToUpper(); z lekcji 08 i pozycja.Przesun(); z lekcji 50 — niezmienny typ zawsze zwraca nową wartość. Kompilator wypisze ostrzeżenie o niewykorzystanym wyniku; przeczytaj je.
Dodawanie i odejmowanie czasu
DateTime start = new DateTime(2026, 5, 12, 8, 0, 0);
Console.WriteLine(start.AddDays(30)); // 11.06.2026 08:00
Console.WriteLine(start.AddDays(-7)); // 05.05.2026 08:00 - w przeszlosc
Console.WriteLine(start.AddHours(2.5)); // 12.05.2026 10:30 - takze ulamki
Console.WriteLine(start.AddMinutes(45)); // 12.05.2026 08:45
Console.WriteLine(start.AddMonths(3)); // 12.08.2026 08:00
Console.WriteLine(start.AddYears(1)); // 12.05.2027 08:00
AddMonths przycina dzień do długości miesiąca
new DateTime(2026, 1, 31).AddMonths(1) daje 28.02.2026, a nie 31 lutego — bo takiej daty nie ma. To jedyne rozsądne zachowanie, ale ma skutek, który zaskakuje: data.AddMonths(1).AddMonths(-1) nie zawsze wraca do punktu wyjścia. Dla 31 stycznia da 28 stycznia. Jeśli liczysz terminy, licz je w dniach, nie w miesiącach.
Różnica dwóch dat to TimeSpan
Odejmowanie dat jest dozwolone i daje odstęp czasu, nie datę:
DateTime wypozyczono = new DateTime(2026, 3, 10);
DateTime dzis = new DateTime(2026, 5, 12);
TimeSpan minelo = dzis - wypozyczono;
Console.WriteLine(minelo.Days); // 63 - pelne dni
Console.WriteLine(minelo.TotalDays); // 63 - to samo, ale jako double
Console.WriteLine(minelo.TotalHours); // 1512
Console.WriteLine(minelo); // 63.00:00:00
Days kontra TotalDays — rozróżnienie, które trzeba znać
TimeSpan t = new TimeSpan(2, 5, 30, 0); // 2 dni, 5 godzin, 30 minut
Console.WriteLine(t.Days); // 2 - CZESC dniowa
Console.WriteLine(t.Hours); // 5 - CZESC godzinowa (0-23)
Console.WriteLine(t.Minutes); // 30 - CZESC minutowa (0-59)
Console.WriteLine(t.TotalDays); // 2,229... - CALOSC w dniach
Console.WriteLine(t.TotalHours); // 53,5 - CALOSC w godzinach
Console.WriteLine(t.TotalMinutes); // 3210 - CALOSC w minutach
| Właściwość | Znaczenie | Typ | Zakres |
|---|---|---|---|
Days, Hours, Minutes, Seconds | część odstępu | int | Hours 0–23, Minutes 0–59 |
TotalDays, TotalHours, TotalMinutes | całość w tej jednostce | double | bez ograniczeń, z częścią ułamkową |
Jak to zapamiętać
„Ile to dni i godzin?” → Days i Hours. „Ile to w ogóle godzin?” → TotalHours. Dla odstępu 53,5 godziny pierwsza para da „2 dni i 5 godzin”, a druga „53,5”. Mylenie ich to częsty błąd w obliczeniach kar za spóźnienie i czasu pracy.
Tworzenie i składanie odstępów
TimeSpan lekcja = TimeSpan.FromMinutes(45);
TimeSpan przerwa = TimeSpan.FromMinutes(10);
TimeSpan dluga = TimeSpan.FromHours(1.5);
TimeSpan tydzien = TimeSpan.FromDays(7);
TimeSpan blok = lekcja + lekcja + przerwa; // odstepy wolno dodawac
Console.WriteLine(blok.TotalMinutes); // 100
DateTime poczatek = new DateTime(2026, 5, 12, 8, 0, 0);
DateTime koniec = poczatek + blok; // data + odstep = data
Console.WriteLine($"{poczatek:HH:mm} - {koniec:HH:mm}"); // 08:00 - 09:40
| Działanie | Wynik | Przykład |
|---|---|---|
| data − data | TimeSpan | ile minęło między dwiema chwilami |
| data + odstęp | DateTime | termin zwrotu |
| data − odstęp | DateTime | data sprzed tygodnia |
| odstęp + odstęp | TimeSpan | łączny czas lekcji |
| odstęp × liczba | TimeSpan | lekcja * 8 — czas ośmiu lekcji |
| data + data | nie istnieje | suma dwóch dat nie ma sensu |
Porównywanie i sortowanie
DateTime a = new DateTime(2026, 5, 12);
DateTime b = new DateTime(2026, 4, 9);
Console.WriteLine(a > b); // True - operatory dzialaja
Console.WriteLine(a == b); // False
Console.WriteLine(a.CompareTo(b)); // 1 - jak w lekcji 51
List<DateTime> terminy = new List<DateTime>
{
new DateTime(2026, 5, 20),
new DateTime(2026, 4, 9),
new DateTime(2026, 5, 18)
};
terminy.Sort(); // DateTime spelnia IComparable (lekcja 48)
foreach (DateTime t in terminy)
{
Console.WriteLine(t.ToString("dd.MM.yyyy"));
}
09.04.2026
18.05.2026
20.05.2026
To właśnie było niemożliwe przy dacie zapisanej jako napis: "09.04.2026" i "20.05.2026" posortowane alfabetycznie dałyby 09 przed 20 przypadkiem, ale "01.01.2027" wylądowałoby na początku.
Jedna chwila, wiele zapisów
Ta sama chwila da się zapisać kilkudziesięcioma sposobami. Wybór zależy od tego, kto to przeczyta: człowiek czy program.
using System.Globalization;
CultureInfo pl = new CultureInfo("pl-PL");
DateTime d = new DateTime(2026, 5, 12, 14, 30, 0);
Console.WriteLine(d.ToString("d", pl)); // 12.05.2026 - krotka data
Console.WriteLine(d.ToString("D", pl)); // 12 maja 2026 - dluga data
Console.WriteLine(d.ToString("t", pl)); // 14:30 - krotka godzina
Console.WriteLine(d.ToString("T", pl)); // 14:30:00 - dluga godzina
Console.WriteLine(d.ToString("g", pl)); // 12.05.2026 14:30
Console.WriteLine(d.ToString("G", pl)); // 12.05.2026 14:30:00
Console.WriteLine(d.ToString("s")); // 2026-05-12T14:30:00 - sortowalny
Console.WriteLine(d.ToString("o")); // 2026-05-12T14:30:00.0000000
Własny format
Console.WriteLine(d.ToString("dd.MM.yyyy")); // 12.05.2026
Console.WriteLine(d.ToString("yyyy-MM-dd")); // 2026-05-12
Console.WriteLine(d.ToString("HH:mm")); // 14:30
Console.WriteLine(d.ToString("dd.MM.yyyy HH:mm")); // 12.05.2026 14:30
Console.WriteLine(d.ToString("dddd, d MMMM yyyy", pl)); // wtorek, 12 maja 2026
Console.WriteLine(d.ToString("MMM", pl)); // maj
// to samo w interpolacji - krocej:
Console.WriteLine($"{d:dd.MM.yyyy}");
Console.WriteLine($"Spotkanie {d:dd.MM} o {d:HH:mm}");
| Znak | Znaczenie | Dla 12 maja 2026, 14:30:05 |
|---|---|---|
d / dd | dzień bez zera / z zerem | 12 / 12 |
ddd / dddd | dzień tygodnia skrót / pełny | wt. / wtorek |
M / MM | miesiąc liczbą | 5 / 05 |
MMM / MMMM | miesiąc nazwą | maj / maja |
yy / yyyy | rok dwucyfrowy / czterocyfrowy | 26 / 2026 |
H / HH | godzina w formacie 24-godzinnym | 14 / 14 |
h / hh | godzina w formacie 12-godzinnym | 2 / 02 |
m / mm | minuty | 30 / 30 |
s / ss | sekundy | 5 / 05 |
Wielkość liter ma znaczenie i myli się każdemu
MM to miesiąc, mm to minuty. dd to dzień, DD nie znaczy nic. HH to godzina 0–23, hh to godzina 1–12 bez informacji, czy jest przed czy po południu. Format "dd.mm.yyyy" skompiluje się i wypisze dzień, minuty i rok — czyli dla naszej chwili 12.30.2026. Taki błąd nie daje żadnego komunikatu.
TryParse, TryParseExact i pułapka 03/04
Zamiana tekstu na datę to ta sama historia co w lekcji 34: dane z zewnątrz, więc TryParse, nigdy Parse.
using System.Globalization;
CultureInfo pl = new CultureInfo("pl-PL");
string[] teksty = { "2026-05-12", "12.05.2026", "31.02.2026", "wczoraj", "" };
foreach (string t in teksty)
{
if (DateTime.TryParse(t, pl, DateTimeStyles.None, out DateTime d))
{
Console.WriteLine($" {t,-12} -> {d:yyyy-MM-dd}");
}
else
{
Console.WriteLine($" {t,-12} -> nie rozpoznano");
}
}
2026-05-12 -> 2026-05-12
12.05.2026 -> 2026-05-12
31.02.2026 -> nie rozpoznano
wczoraj -> nie rozpoznano
-> nie rozpoznano
Zwróć uwagę na trzeci wiersz: 31.02.2026 ma poprawny kształt, a mimo to zostaje odrzucone, bo takiego dnia nie ma. Napis tego nie sprawdziłby nigdy.
Pułapka, która kosztuje najwięcej
Co znaczy 03/04/2026? Zależy, kto pisał:
string tekst = "03/04/2026";
DateTime.TryParseExact(tekst, "dd/MM/yyyy", CultureInfo.InvariantCulture,
DateTimeStyles.None, out DateTime poEuropejsku);
DateTime.TryParseExact(tekst, "MM/dd/yyyy", CultureInfo.InvariantCulture,
DateTimeStyles.None, out DateTime poAmerykansku);
Console.WriteLine($"jako dd/MM: {poEuropejsku:yyyy-MM-dd}"); // 2026-04-03 - 3 kwietnia
Console.WriteLine($"jako MM/dd: {poAmerykansku:yyyy-MM-dd}"); // 2026-03-04 - 4 marca
Ten sam napis, dwie różne daty
Miesiąc różnicy i żadnego błędu. Jeśli wczytujesz daty z pliku, który powstał na innym komputerze, musisz wiedzieć, w jakim formacie są zapisane — i podać go jawnie przez TryParseExact. TryParse użyje ustawień tego komputera, więc ten sam program da różne wyniki w szkole i u ucznia w domu.
TryParseExact — gdy znasz format
string zPliku = "2026-05-12";
if (DateTime.TryParseExact(zPliku, "yyyy-MM-dd", CultureInfo.InvariantCulture,
DateTimeStyles.None, out DateTime data))
{
Console.WriteLine($"wczytano: {data:dd.MM.yyyy}");
}
else
{
Console.WriteLine("zly format daty w pliku");
}
// kilka dopuszczalnych formatow naraz:
string[] formaty = { "yyyy-MM-dd", "dd.MM.yyyy", "d.M.yyyy" };
if (DateTime.TryParseExact("12.5.2026", formaty, CultureInfo.InvariantCulture,
DateTimeStyles.None, out DateTime druga))
{
Console.WriteLine($"wczytano: {druga:dd.MM.yyyy}");
}
| Metoda | Kiedy użyć | Ryzyko |
|---|---|---|
DateTime.Parse | nigdy dla danych z zewnątrz | wyjątek przy każdym złym wpisie |
DateTime.TryParse | wpis od użytkownika w jego ustawieniach | wynik zależy od komputera |
DateTime.TryParseExact | plik, sieć, baza — format jest znany | brak, jeśli format podany poprawnie |
TryParseExact z tablicą formatów | wpis użytkownika w kilku dopuszczalnych postaciach | brak |
Zasada z lekcji 36, ta sama dla dat
Do pliku zapisuj daty w formacie "yyyy-MM-dd" albo "o", a odczytuj przez TryParseExact z InvariantCulture. Taki zapis jest jednoznaczny na całym świecie, sortuje się poprawnie jako tekst i nie zależy od ustawień regionalnych. Format dla człowieka (12 maja 2026) i format do zapisania w pliku (2026-05-12) to dwie różne rzeczy — tak samo jak przecinek i kropka w liczbach.
Gdy godzina jest zbędna albo kłopotliwa
DateTime zawsze nosi godzinę, także wtedy, gdy nikomu nie jest potrzebna. Data urodzenia o 00:00:00 to drobna nieprawda, która potrafi zaszkodzić: porównanie DateTime.Now z datą urodzenia nigdy nie będzie równe, bo różnią się o kilka godzin.
Od .NET 6 są dwa typy, które tego nie robią:
DateOnly urodziny = new DateOnly(2008, 9, 23); // tylko data
TimeOnly dzwonek = new TimeOnly(8, 0); // tylko godzina
Console.WriteLine(urodziny); // 23.09.2008
Console.WriteLine(dzwonek); // 08:00
DateOnly dzis = DateOnly.FromDateTime(DateTime.Now);
TimeOnly teraz = TimeOnly.FromDateTime(DateTime.Now);
Console.WriteLine(urodziny.AddYears(18)); // 23.09.2026
Console.WriteLine(dzwonek.AddMinutes(45)); // 08:45
Liczenie różnicy dni
DateOnly ma właściwość DayNumber — numer dnia liczony od 1 stycznia roku 1. Różnica tych numerów to po prostu liczba dni:
DateOnly urodziny = new DateOnly(2008, 9, 23);
DateOnly dzis = new DateOnly(2026, 5, 12);
int dni = dzis.DayNumber - urodziny.DayNumber;
Console.WriteLine($"zyje {dni} dni"); // 6440
// wiek w pelnych latach - uwaga, to nie jest zwykle odejmowanie lat:
int lat = dzis.Year - urodziny.Year;
if (dzis < urodziny.AddYears(lat)) // urodziny w tym roku jeszcze nie byly
{
lat--;
}
Console.WriteLine($"ma {lat} lat"); // 17
Wiek to nie odjęcie lat
2026 - 2008 daje 18, ale 12 maja 2026 ta osoba ma jeszcze 17 lat — urodziny wypadają we wrześniu. Dlatego po odjęciu lat trzeba sprawdzić, czy urodziny w tym roku już były. Ten sam błąd popełnia się przy stażu pracy i okresie gwarancji.
Który typ wybrać
| Co opisujesz | Typ | Dlaczego |
|---|---|---|
| data urodzenia, termin zwrotu, dzień egzaminu | DateOnly | godzina nie istnieje, więc niech jej nie będzie |
| godzina dzwonka, start lekcji | TimeOnly | powtarza się codziennie, nie dotyczy jednego dnia |
| chwila zdarzenia — kiedy wypożyczono, kiedy zapisano plik | DateTime | potrzebna data i godzina |
| ile trwa lekcja, ile dni spóźnienia | TimeSpan | to odstęp, nie moment |
| chwila, która musi być jednoznaczna między strefami | DateTimeOffset | nosi ze sobą przesunięcie względem czasu uniwersalnego |
Czas lokalny czy uniwersalny
DateTime pamięta w polu Kind, skąd pochodzi — i to pole bardzo łatwo zgubić:
Console.WriteLine(DateTime.Now.Kind); // Local
Console.WriteLine(DateTime.UtcNow.Kind); // Utc
Console.WriteLine(new DateTime(2026, 5, 12).Kind); // Unspecified <- uwaga
DateTime lokalnie = DateTime.Now;
DateTime uniwersalnie = lokalnie.ToUniversalTime(); // w Polsce -1 h (zima) lub -2 h (lato)
DateTime znowuLokalnie = uniwersalnie.ToLocalTime();
Console.WriteLine(TimeZoneInfo.Local.StandardName); // nazwa strefy tego komputera
Co z tego wynika w praktyce szkolnej
W programie konsolowym działającym na jednym komputerze DateTime.Now wystarcza i nie ma się czym przejmować. Problem zaczyna się, gdy dane trafiają do pliku odczytywanego gdzie indziej albo gdy przychodzi zmiana czasu — godzina 2:30 w ostatnią noc października istnieje dwa razy, a w marcową noc nie istnieje wcale. Reguła zawodowa jest jedna: zapisuj czas uniwersalny (UtcNow) i przeliczaj na lokalny tylko przy wypisywaniu. Do przechowywania chwil najlepszy jest DateTimeOffset, który nosi przesunięcie strefy w sobie.
Terminarz biblioteki szkolnej
Terminarz biblioteki szkolnej. Program liczy terminy zwrotu, wykrywa spóźnienia i kary, buduje plan dnia z odstępów czasu, liczy wiek z DateOnly, pokazuje siedem zapisów tej samej chwili i zapisuje daty do pliku w formacie, który odczyta się na każdym komputerze.
Dlaczego data „dzisiejsza” jest wpisana na stałe
Gdyby program używał DateTime.Today, wynik byłby inny każdego dnia i nie dałoby się go porównać z tym, co jest poniżej. Dlatego na początku stoi new DateTime(2026, 5, 12) z komentarzem. To zwykła technika testowania: ustalamy czas, żeby wynik był powtarzalny. W prawdziwym programie byłby tam odczyt z zegara.
// Program.cs - pelny plik
using System.Globalization;
CultureInfo pl = new CultureInfo("pl-PL");
// Data "dzisiejsza" jest ustalona na stale, zeby wynik programu byl
// powtarzalny. W prawdziwym programie bylby tu DateTime.Today.
DateTime dzisiaj = new DateTime(2026, 5, 12);
decimal karaZaDzien = 0.20m;
Console.WriteLine("=== 1. co wiemy o dzisiejszej dacie ===");
Console.WriteLine($" data: {dzisiaj:dd.MM.yyyy}");
Console.WriteLine($" slownie: {dzisiaj.ToString("dddd, d MMMM yyyy", pl)}");
Console.WriteLine($" dzien roku: {dzisiaj.DayOfYear}");
Console.WriteLine($" dni w miesiacu: {DateTime.DaysInMonth(dzisiaj.Year, dzisiaj.Month)}");
Console.WriteLine($" rok przestepny: {DateTime.IsLeapYear(dzisiaj.Year)}");
Console.WriteLine($" weekend: {CzyWeekend(dzisiaj)}");
// ---------- 2. terminy zwrotu ----------
List<Wypozyczenie> wypozyczenia = new List<Wypozyczenie>
{
new Wypozyczenie("Pan Tadeusz", "Kasia Nowak", new DateTime(2026, 4, 20), 30),
new Wypozyczenie("Lalka", "Marek Lis", new DateTime(2026, 5, 5), 14),
new Wypozyczenie("Ferdydurke", "Ala Wrona", new DateTime(2026, 3, 10), 30),
new Wypozyczenie("Solaris", "Piotr Dab", new DateTime(2026, 5, 11), 7)
};
Console.WriteLine();
Console.WriteLine("=== 2. terminy zwrotu (wg daty terminu) ===");
wypozyczenia.Sort(delegate (Wypozyczenie a, Wypozyczenie b)
{
return a.Termin.CompareTo(b.Termin);
});
decimal sumaKar = 0m;
foreach (Wypozyczenie w in wypozyczenia)
{
int dni = w.DniDoTerminu(dzisiaj);
decimal kara = w.Kara(dzisiaj, karaZaDzien);
sumaKar += kara;
string stan;
if (dni < 0)
{
stan = $"PO TERMINIE o {-dni} dni, kara {kara:F2} zl";
}
else if (dni == 0)
{
stan = "zwrot DZISIAJ";
}
else
{
stan = $"zostalo {dni} dni";
}
Console.WriteLine($" {w.Tytul,-13} {w.Kto,-12} termin {w.Termin:dd.MM.yyyy} {stan}");
}
Console.WriteLine($" suma kar: {sumaKar:F2} zl");
// ---------- 3. odstepy czasu ----------
Console.WriteLine();
Console.WriteLine("=== 3. plan dnia z TimeSpan ===");
TimeSpan lekcja = TimeSpan.FromMinutes(45);
TimeSpan przerwa = TimeSpan.FromMinutes(10);
DateTime godzina = new DateTime(2026, 5, 12, 8, 0, 0);
for (int nr = 1; nr <= 4; nr++)
{
DateTime koniec = godzina + lekcja;
Console.WriteLine($" lekcja {nr}: {godzina:HH:mm} - {koniec:HH:mm}");
godzina = koniec + przerwa;
}
TimeSpan calosc = (lekcja + przerwa) * 4 - przerwa;
Console.WriteLine($" razem: {calosc.TotalMinutes} min = {calosc.Hours} h {calosc.Minutes} min");
// ---------- 4. wiek z DateOnly ----------
Console.WriteLine();
Console.WriteLine("=== 4. wiek uczennicy (DateOnly) ===");
DateOnly urodziny = new DateOnly(2008, 9, 23);
DateOnly dzisDate = DateOnly.FromDateTime(dzisiaj);
int dniZycia = dzisDate.DayNumber - urodziny.DayNumber;
int lat = dzisDate.Year - urodziny.Year;
if (dzisDate < urodziny.AddYears(lat))
{
lat--;
}
Console.WriteLine($" urodzona: {urodziny:dd.MM.yyyy}");
Console.WriteLine($" dni zycia: {dniZycia}");
Console.WriteLine($" pelnych lat: {lat}");
Console.WriteLine($" 18 lat skonczy: {urodziny.AddYears(18):dd.MM.yyyy}");
// ---------- 5. formatowanie ----------
Console.WriteLine();
Console.WriteLine("=== 5. jedna chwila, siedem zapisow ===");
DateTime chwila = new DateTime(2026, 5, 12, 14, 30, 0);
Console.WriteLine($" dla czlowieka: {chwila.ToString("dddd, d MMMM yyyy, HH:mm", pl)}");
Console.WriteLine($" krotko: {chwila.ToString("g", pl)}");
Console.WriteLine($" tylko data: {chwila:dd.MM.yyyy}");
Console.WriteLine($" tylko godzina: {chwila:HH:mm}");
Console.WriteLine($" do pliku: {chwila:yyyy-MM-dd}");
Console.WriteLine($" sortowalnie: {chwila:s}");
Console.WriteLine($" blednie (mm): {chwila:dd.mm.yyyy}");
// ---------- 6. zapis i odczyt z pliku ----------
Console.WriteLine();
Console.WriteLine("=== 6. daty w pliku ===");
string folder = Path.Combine(AppContext.BaseDirectory, "dane");
Directory.CreateDirectory(folder);
string plik = Path.Combine(folder, "wypozyczenia.csv");
List<string> doZapisu = new List<string>();
foreach (Wypozyczenie w in wypozyczenia)
{
doZapisu.Add(w.DoWiersza());
}
File.WriteAllLines(plik, doZapisu);
Console.WriteLine($" zapisano {doZapisu.Count} wierszy");
Console.WriteLine($" pierwszy wiersz: {doZapisu[0]}");
int wczytane = 0;
int odrzucone = 0;
foreach (string wiersz in File.ReadAllLines(plik))
{
string[] pola = wiersz.Split(';');
if (pola.Length != 4)
{
odrzucone++;
continue;
}
if (!DateTime.TryParseExact(pola[2], "yyyy-MM-dd", CultureInfo.InvariantCulture,
DateTimeStyles.None, out DateTime data))
{
Console.WriteLine($" zla data w wierszu: {pola[2]}");
odrzucone++;
continue;
}
if (!int.TryParse(pola[3], out int dniNaZwrot))
{
odrzucone++;
continue;
}
wczytane++;
}
Console.WriteLine($" wczytano {wczytane}, odrzucono {odrzucone}");
// ---------- 7. niezmiennosc ----------
Console.WriteLine();
Console.WriteLine("=== 7. DateTime jest niezmienny ===");
DateTime proba = new DateTime(2026, 4, 20);
proba.AddDays(30);
Console.WriteLine($" bez przypisania: {proba:dd.MM.yyyy}");
proba = proba.AddDays(30);
Console.WriteLine($" z przypisaniem: {proba:dd.MM.yyyy}");
DateTime koniecStycznia = new DateTime(2026, 1, 31);
Console.WriteLine($" 31.01 + 1 miesiac: {koniecStycznia.AddMonths(1):dd.MM.yyyy}");
Console.WriteLine($" i z powrotem - 1 miesiac: {koniecStycznia.AddMonths(1).AddMonths(-1):dd.MM.yyyy}");
static bool CzyWeekend(DateTime d)
{
return d.DayOfWeek == DayOfWeek.Saturday || d.DayOfWeek == DayOfWeek.Sunday;
}
class Wypozyczenie
{
public string Tytul { get; }
public string Kto { get; }
public DateTime DataWypozyczenia { get; }
public int DniNaZwrot { get; }
public Wypozyczenie(string tytul, string kto, DateTime data, int dniNaZwrot)
{
Tytul = tytul;
Kto = kto;
DataWypozyczenia = data;
DniNaZwrot = dniNaZwrot;
}
public DateTime Termin
{
get { return DataWypozyczenia.AddDays(DniNaZwrot); }
}
public int DniDoTerminu(DateTime dzisiaj)
{
TimeSpan roznica = Termin.Date - dzisiaj.Date;
return roznica.Days;
}
public decimal Kara(DateTime dzisiaj, decimal zaDzien)
{
int spoznienie = -DniDoTerminu(dzisiaj);
if (spoznienie <= 0)
{
return 0m;
}
return spoznienie * zaDzien;
}
public string DoWiersza()
{
return string.Join(";", Tytul, Kto,
DataWypozyczenia.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
DniNaZwrot.ToString(CultureInfo.InvariantCulture));
}
public override string ToString()
{
return $"{Tytul} ({Kto}), termin {Termin:dd.MM.yyyy}";
}
}
=== 1. co wiemy o dzisiejszej dacie ===
data: 12.05.2026
slownie: wtorek, 12 maja 2026
dzien roku: 132
dni w miesiacu: 31
rok przestepny: False
weekend: False
=== 2. terminy zwrotu (wg daty terminu) ===
Ferdydurke Ala Wrona termin 09.04.2026 PO TERMINIE o 33 dni, kara 6,60 zl
Solaris Piotr Dab termin 18.05.2026 zostalo 6 dni
Lalka Marek Lis termin 19.05.2026 zostalo 7 dni
Pan Tadeusz Kasia Nowak termin 20.05.2026 zostalo 8 dni
suma kar: 6,60 zl
=== 3. plan dnia z TimeSpan ===
lekcja 1: 08:00 - 08:45
lekcja 2: 08:55 - 09:40
lekcja 3: 09:50 - 10:35
lekcja 4: 10:45 - 11:30
razem: 210 min = 3 h 30 min
=== 4. wiek uczennicy (DateOnly) ===
urodzona: 23.09.2008
dni zycia: 6440
pelnych lat: 17
18 lat skonczy: 23.09.2026
=== 5. jedna chwila, siedem zapisow ===
dla czlowieka: wtorek, 12 maja 2026, 14:30
krotko: 12.05.2026 14:30
tylko data: 12.05.2026
tylko godzina: 14:30
do pliku: 2026-05-12
sortowalnie: 2026-05-12T14:30:00
blednie (mm): 12.30.2026
=== 6. daty w pliku ===
zapisano 4 wierszy
pierwszy wiersz: Ferdydurke;Ala Wrona;2026-03-10;30
wczytano 4, odrzucono 0
=== 7. DateTime jest niezmienny ===
bez przypisania: 20.04.2026
z przypisaniem: 20.05.2026
31.01 + 1 miesiac: 28.02.2026
i z powrotem - 1 miesiac: 28.01.2026
Co dzieje się po kolei
- Blok 1. Z jednej daty program odczytuje dzień tygodnia, numer dnia w roku i długość miesiąca — wszystko gotowymi właściwościami. Przy dacie zapisanej jako napis każda z tych linii byłaby osobnym algorytmem.
- Blok 2.
Terminto właściwość wyliczana:DataWypozyczenia.AddDays(DniNaZwrot). Dla Ferdydurke 10 marca plus 30 dni daje 9 kwietnia — program sam przeszedł przez koniec marca. - Sortowanie po terminie działa, bo
DateTimespełnia umowęIComparablez lekcji 48, a metoda porównująca to jedna linia zCompareTo(lekcja 51). DniDoTerminuodejmujeTermin.Dateoddzisiaj.Date. Porównanie samych dat, bez godzin, jest tu konieczne: gdyby jedna z wartości miała godzinę 14:30, różnica wyszłaby o dzień mniejsza.- Ujemna liczba dni znaczy „po terminie”. Ferdydurke jest spóźniona o 33 dni, więc kara to 33 × 0,20 = 6,60 zł.
- Blok 3. Pętla dodaje do godziny kolejne odstępy. Zauważ, że
godzinajest przypisywana na nowo w każdym obiegu — bez tego nic by się nie przesuwało. (lekcja + przerwa) * 4 - przerwato 4 × 55 minus 10 = 210 minut. Wypisane przezHoursiMinutesdaje „3 h 30 min”, a przezTotalMinutes— 210. Dwa poprawne odczyty tej samej wartości.- Blok 4. Wiek nie jest odjęciem lat: 2026 − 2008 daje 18, ale urodziny wypadają we wrześniu, więc warunek cofa wynik do 17.
- Blok 5. Ostatni wiersz pokazuje błąd, o którym mówi sekcja o formatowaniu.
"dd.mm.yyyy"wypisało12.30.2026— dzień, minuty i rok. Program nie zgłosił niczego. - Blok 6. Do pliku data idzie jako
2026-03-10, a odczytuje się ją przezTryParseExactzInvariantCulture. Ten plik otworzy się poprawnie i w Polsce, i w Stanach. - Blok 7.
proba.AddDays(30);bez przypisania nie zmieniło niczego. AAddMonths(1).AddMonths(-1)dla 31 stycznia dało 28 stycznia — bo pierwszy krok przyciął dzień do 28 lutego i drugi już nie wiedział, że miało być 31.
Sprawdź pięć rzeczy
- Zmień ustaloną datę na
DateTime.Todayi uruchom program. Wszystkie terminy przeliczą się na dzisiejszy dzień, a Ferdydurke będzie spóźniona o jeszcze więcej dni. - W metodzie
DniDoTerminuusuń oba.Datei podstaw jako „dzisiaj” wartośćnew DateTime(2026, 5, 12, 14, 30, 0). Liczby dni zmniejszą się o jeden — bo program liczy teraz pełne doby, a nie dni kalendarzowe. - W bloku 3 usuń przypisanie
godzina = koniec + przerwa;. Wszystkie cztery lekcje zaczną się o 08:00, boDateTimenie zmienia się sam. - W bloku 6 zmień format zapisu daty na
"dd.MM.yyyy", nie ruszając odczytu. Wszystkie cztery wiersze zostaną odrzucone z komunikatem o złej dacie — zapis i odczyt muszą używać tego samego formatu. - Dopisz w bloku 4 drugą datę urodzenia, która w 2026 roku już minęła — na przykład 3 marca 2008. Sprawdź, że wiek wychodzi 18, a nie 17, i zrozum, dlaczego warunek tym razem się nie wykonał.
Zestawienie elementów
| Element | Wynik | Działanie i ograniczenia |
|---|---|---|
DateTime.Now | DateTime | Data i godzina z zegara, czas lokalny. |
DateTime.Today | DateTime | Dzisiejsza data z godziną 00:00:00. |
DateTime.UtcNow | DateTime | Czas uniwersalny — do zapisywania chwil. |
new DateTime(r, m, d) | DateTime | Kolejność od największej jednostki. Zła data daje ArgumentOutOfRangeException. |
.Year, .Month, .Day | int | Składniki daty. |
.Hour, .Minute, .Second | int | Składniki godziny. |
.DayOfWeek | DayOfWeek | Typ wyliczeniowy; Sunday ma wartość 0. |
.DayOfYear | int | Który to dzień roku, 1–366. |
.Date | DateTime | Sama data, godzina ustawiona na 00:00. Do porównywania dni. |
.TimeOfDay | TimeSpan | Sama godzina jako odstęp od północy. |
DateTime.DaysInMonth(r, m) | int | Długość miesiąca, z uwzględnieniem lat przestępnych. |
DateTime.IsLeapYear(r) | bool | Zna pełną regułę, także wyjątek dla lat podzielnych przez 100. |
.AddDays(n), .AddHours(n)… | DateTime | Zwraca nową wartość — trzeba ją przypisać. n może być ujemne. |
.AddMonths(n), .AddYears(n) | DateTime | Przycina dzień do długości miesiąca docelowego. |
data1 - data2 | TimeSpan | Odstęp między chwilami; może być ujemny. |
data + odstęp | DateTime | Nowa chwila. |
TimeSpan.FromMinutes(n) | TimeSpan | Także FromDays, FromHours, FromSeconds. |
.Days, .Hours, .Minutes | int | Części odstępu. Hours mieści się w 0–23. |
.TotalDays, .TotalHours… | double | Całość w tej jednostce, z częścią ułamkową. |
.ToString("dd.MM.yyyy") | string | MM miesiąc, mm minuty — wielkość liter jest istotna. |
.ToString("d", kultura) | string | Formaty standardowe: d, D, t, T, g, G, s, o. |
DateTime.TryParse(t, k, s, out d) | bool | Wynik zależy od ustawień regionalnych. |
DateTime.TryParseExact(…) | bool | Format podany jawnie — do danych z pliku i sieci. |
new DateOnly(r, m, d) | DateOnly | Sama data, bez godziny. Od .NET 6. |
.DayNumber | int | Numer dnia; różnica dwóch numerów to liczba dni. |
new TimeOnly(g, m) | TimeOnly | Sama godzina, bez daty. Od .NET 6. |
DateOnly.FromDateTime(d) | DateOnly | Odrzuca część godzinową. Jest też TimeOnly.FromDateTime. |
.Kind | DateTimeKind | Local, Utc albo Unspecified. |
.ToUniversalTime(), .ToLocalTime() | DateTime | Przeliczenie między czasem lokalnym a uniwersalnym. |
Zanim utkniesz
| Zapis | Problem |
|---|---|
Data przechowywana jako string | Nie da się odjąć, posortować ani sprawdzić poprawności. Napis przyjmie 31.02.2026. |
data.AddDays(30); bez przypisania | Kompiluje się i nic nie robi. DateTime jest niezmienny — wynik trzeba przypisać. |
"dd.mm.yyyy" zamiast "dd.MM.yyyy" | Wypisze dzień, minuty i rok. Żadnego komunikatu, same złe daty. |
"hh:mm" dla godziny z zegara 24-godzinnego | hh daje 1–12 bez informacji o połowie dnia — 14:30 wypisze się jako 02:30. Używaj HH. |
DateTime.Parse dla danych z zewnątrz | Wyjątek przy pierwszym złym wpisie. Zawsze TryParse albo TryParseExact. |
TryParse dla daty z pliku | Wynik zależy od ustawień komputera. 03/04/2026 to 3 kwietnia albo 4 marca — miesiąc różnicy bez błędu. |
Zapis daty do pliku w formacie dd.MM.yyyy | Plik nie da się jednoznacznie odczytać na innym komputerze. Do plików yyyy-MM-dd z InvariantCulture. |
Porównywanie dni bez .Date | DateTime.Now ma godzinę, więc „dziś” nigdy nie będzie równe dacie z północy. Porównuj a.Date == b.Date. |
TotalDays tam, gdzie chodzi o Days | Dla 53,5 godziny Days daje 2, a TotalDays — 2,23. Pierwsze to część, drugie całość. |
Liczenie wieku jako rokDzis - rokUrodzenia | Daje o rok za dużo, dopóki urodziny w tym roku nie minęły. Trzeba dopisać sprawdzenie. |
| Liczenie terminów w miesiącach | AddMonths(1).AddMonths(-1) dla 31 stycznia daje 28 stycznia. Terminy licz w dniach. |
Własny warunek rok % 4 == 0 na rok przestępny | Nie uwzględnia wyjątku dla lat podzielnych przez 100. Użyj DateTime.IsLeapYear. |
(int)d.DayOfWeek jako numer dnia tygodnia | Sunday ma wartość 0, więc poniedziałek wypada na 1, a niedziela na 0. W polskim kalendarzu przelicz: ((int)d.DayOfWeek + 6) % 7. |
| Ręczne dodawanie 24 godzin jako „jeden dzień” | W noce zmiany czasu doba ma 23 albo 25 godzin. Używaj AddDays(1). |
Zapisywanie DateTime.Now do pliku z chwilami zdarzeń | Po zmianie czasu albo odczycie w innej strefie wartość przestaje być jednoznaczna. Zapisuj UtcNow albo DateTimeOffset. |
Zadania
Napisz program, który tworzy datę new DateTime(2026, 5, 12) i wypisuje o niej osiem informacji: datę w formacie dd.MM.yyyy, datę słownie (dzień tygodnia, dzień, nazwa miesiąca i rok, z kulturą pl-PL), numer dnia w roku, liczbę dni w tym miesiącu, czy rok jest przestępny, datę o 30 dni późniejszą, datę o tydzień wcześniejszą oraz datę ostatniego dnia tego miesiąca. Następnie zmień datę na 29.02.2024 i uruchom ponownie; potem spróbuj 29.02.2026 i zapisz w komentarzu nazwę wyjątku, który dostaniesz, oraz dlaczego pierwsza data była poprawna, a druga nie.
Napisz program, który wczytuje od użytkownika datę w formacie dd.MM.yyyy, korzystając z DateTime.TryParseExact z CultureInfo.InvariantCulture, i pyta o nią w pętli, dopóki nie dostanie poprawnej (wzorzec z lekcji 34). Potem wypisz: ile pełnych dni dzieli ją od DateTime.Today, czy wypada w przeszłości, czy w przyszłości, jaki to dzień tygodnia oraz ile pełnych tygodni to jest. Sprawdź program dla daty wczorajszej, dzisiejszej i dla 31.02.2026, i zapisz wyniki w komentarzu. Pamiętaj o użyciu .Date przy odejmowaniu.
Napisz program, który wypisuje plan lekcji od godziny 8:00: osiem lekcji po 45 minut, przerwy po 10 minut, a po trzeciej lekcji przerwa obiadowa 20 minut. Użyj TimeSpan.FromMinutes, dodawania do DateTime i pętli for. Dla każdej lekcji wypisz jej numer oraz godzinę rozpoczęcia i zakończenia w formacie HH:mm. Na końcu podaj: godzinę zakończenia ostatniej lekcji, łączny czas zajęć bez przerw oraz łączny czas pobytu w szkole, każdy raz przez TotalMinutes i raz w postaci „X h Y min” przez Hours i Minutes. W komentarzu wyjaśnij różnicę między tymi dwoma odczytami.
Napisz program, który dla chwili new DateTime(2026, 5, 12, 14, 30, 5) wypisuje ją w ośmiu postaciach: formatami standardowymi d, D, t, T, g i s z kulturą pl-PL oraz własnymi formatami "yyyy-MM-dd HH:mm:ss" i "dddd, d MMMM yyyy". Następnie wypisz tę samą chwilę formatami "dd.mm.yyyy" i "dd.MM.yyyy" obok siebie i zapisz w komentarzu, czym się różnią wyniki i dlaczego. Na koniec wypisz ją formatami "hh:mm" i "HH:mm" i wyjaśnij w komentarzu, który z nich jest bezpieczny dla godzin po południu.
Napisz program, który dla napisu "03/04/2026" wykonuje DateTime.TryParseExact dwa razy: raz z formatem "dd/MM/yyyy", raz z "MM/dd/yyyy", oba razy z CultureInfo.InvariantCulture. Wypisz obie daty w formacie yyyy-MM-dd wraz z nazwą miesiąca po polsku. Następnie napisz metodę static bool WczytajDate(string tekst, out DateTime data), która przyjmuje datę w jednym z trzech formatów: "yyyy-MM-dd", "dd.MM.yyyy" i "d.M.yyyy" — użyj wersji TryParseExact z tablicą formatów. Przetestuj ją na sześciu napisach: trzech poprawnych w każdym z formatów, jednym w formacie "03/04/2026", jednym "31.02.2026" i jednym pustym. W komentarzu napisz, dlaczego do zapisu daty w pliku wybiera się format yyyy-MM-dd.
Napisz program prowadzący terminarz wypożyczeń w bibliotece szkolnej, z trwałą pamięcią w pliku. Zadeklaruj klasę Wypozyczenie z właściwościami Tytul, Czytelnik, DataWypozyczenia (typ DateOnly), DniNaZwrot oraz DataZwrotu typu DateOnly? — brak wartości znaczy „jeszcze nie oddane” (lekcja 34). Dodaj wyliczaną właściwość Termin, metodę int DniDoTerminu(DateOnly dzisiaj) liczoną przez DayNumber, metodę decimal Kara(DateOnly dzisiaj, decimal zaDzien) naliczającą 0,20 zł za każdy dzień spóźnienia (po zwrocie liczoną do daty zwrotu, a nie do dzisiaj), metodę DoWiersza() zapisującą wszystkie pola średnikami z datami w formacie yyyy-MM-dd (puste pole dla braku zwrotu) oraz przesłonięte ToString(). Napisz metodę parsującą zwracającą Wypozyczenie? z powodem odrzucenia w parametrze out, używającą TryParseExact z InvariantCulture. Program ma: wczytać plik dane/wypozyczenia.csv, jeśli istnieje, i podać, ile wpisów przyjęto i ile odrzucono; pokazać menu w pętli z opcjami dodania wypożyczenia (data domyślnie dzisiejsza, z możliwością podania innej), zarejestrowania zwrotu, wypisania wszystkich wypożyczeń posortowanych po terminie, wypisania tylko przeterminowanych, zestawienia kar oraz zakończenia. Lista wypisywana ma mieć kolumny: tytuł, czytelnik, data wypożyczenia, termin, stan (zwrócone w dniu / zostało N dni / po terminie o N dni) oraz kara. Zestawienie kar podaje sumę oraz czytelnika z największą karą. Każde dodanie i każdy zwrot zapisuj do pliku od razu. Daty wczytywane od użytkownika sprawdzaj w pętli do poprawnej odpowiedzi. Na końcu pliku źródłowego, w komentarzu, odpowiedz na cztery pytania: dlaczego DataWypozyczenia jest typu DateOnly, a nie DateTime; dlaczego DataZwrotu ma znak zapytania; dlaczego różnicę dni liczysz przez DayNumber, a nie przez odejmowanie lat i miesięcy; oraz dlaczego w pliku daty zapisane są inaczej niż na ekranie.
Co trzeba zapamiętać
- Data to typ, nie napis:
DateTimeumie odejmować, porównywać, sortować i odrzucać daty nieistniejące. new DateTime(rok, miesiąc, dzień)— kolejność zawsze od największej jednostki.DateTimejest niezmienną strukturą, więc każda metodaAdd…zwraca nową wartość do przypisania.AddMonthsprzycina dzień do długości miesiąca, więc terminy licz w dniach.- Różnica dwóch dat to
TimeSpan; data plusTimeSpanto znowu data. Daysto część odstępu,TotalDaysto całość w dniach — to dwie różne liczby.- Porównując dni, używaj
.Date, boDateTime.Nownosi ze sobą godzinę. - W formatach
MMto miesiąc,mmto minuty,HHto godzina 0–23, ahh— 1–12. - Format dla człowieka i format do pliku to dwie różne rzeczy: do pliku
yyyy-MM-ddzInvariantCulture. TryParseczyta według ustawień komputera,TryParseExactwedług podanego formatu — do plików tylko to drugie.03/04/2026to 3 kwietnia albo 4 marca; bez jawnego formatu nie da się tego rozstrzygnąć.DateOnlydo dat bez godziny,TimeOnlydo godzin bez daty,TimeSpando odstępów.- Różnicę dni dla
DateOnlypoliczysz, odejmując wartościDayNumber. - Wiek to nie odjęcie lat — trzeba sprawdzić, czy urodziny w tym roku już były.
- Chwile zdarzeń zapisuj w czasie uniwersalnym, a na lokalny przeliczaj dopiero przy wypisywaniu.
Dokumentacja: Microsoft Learn — temat tej lekcji.