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.

C# DateTime TimeSpan DateOnly TryParseExact formatowanie dat 90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wyjaśnisz, dlaczego data zapisana jako napis albo jako trzy liczby jest złym rozwiązaniem.
  • Utworzysz DateTime z 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 Days od TotalDays.
  • Złożysz plan dnia z odstępów TimeSpan i dodasz je do godziny.
  • Sformatujesz datę dla człowieka i dla pliku oraz wskażesz różnicę między MM i mm.
  • Wczytasz datę z tekstu przez TryParse i TryParseExact i uzasadnisz wybór.
  • Wybierzesz między DateTime, DateOnly, TimeOnly i TimeSpan.
  • 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.

TEORIA

Data jako napis i sześć pytań bez odpowiedzi

Biblioteka szkolna zapisuje wypożyczenia. Każde ma datę i termin zwrotu. Pierwszy pomysł to napis:

data jako 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:

PytaniePrzy 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:

Program.cs
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.

TEORIA

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

Program.cs
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ć

Program.cs
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:

Program.cs
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

Program.cs
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.

TEORIA

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ą.

Program.cs
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

Program.cs
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ę:

Program.cs
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ć

Program.cs
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śćZnaczenieTypZakres
Days, Hours, Minutes, Secondsczęść odstępuintHours 0–23, Minutes 0–59
TotalDays, TotalHours, TotalMinutescałość w tej jednostcedoublebez 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

Program.cs
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łanieWynikPrzykład
data − dataTimeSpanile minęło między dwiema chwilami
data + odstępDateTimetermin zwrotu
data − odstępDateTimedata sprzed tygodnia
odstęp + odstępTimeSpanłączny czas lekcji
odstęp × liczbaTimeSpanlekcja * 8 — czas ośmiu lekcji
data + datanie istniejesuma dwóch dat nie ma sensu

Porównywanie i sortowanie

Program.cs
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"));
}
wynik w konsoli
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.

ELEMENTY WBUDOWANE

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.

Program.cs
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

Program.cs
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}");
ZnakZnaczenieDla 12 maja 2026, 14:30:05
d / dddzień bez zera / z zerem12 / 12
ddd / dddddzień tygodnia skrót / pełnywt. / wtorek
M / MMmiesiąc liczbą5 / 05
MMM / MMMMmiesiąc nazwąmaj / maja
yy / yyyyrok dwucyfrowy / czterocyfrowy26 / 2026
H / HHgodzina w formacie 24-godzinnym14 / 14
h / hhgodzina w formacie 12-godzinnym2 / 02
m / mmminuty30 / 30
s / sssekundy5 / 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.

TEORIA

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.

Program.cs
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");
    }
}
wynik w konsoli
  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ł:

Program.cs
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

Program.cs
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}");
}
MetodaKiedy użyćRyzyko
DateTime.Parsenigdy dla danych z zewnątrzwyjątek przy każdym złym wpisie
DateTime.TryParsewpis od użytkownika w jego ustawieniachwynik zależy od komputera
DateTime.TryParseExactplik, sieć, baza — format jest znanybrak, jeśli format podany poprawnie
TryParseExact z tablicą formatówwpis użytkownika w kilku dopuszczalnych postaciachbrak

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.

TEORIA

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ą:

Program.cs
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:

Program.cs
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 opisujeszTypDlaczego
data urodzenia, termin zwrotu, dzień egzaminuDateOnlygodzina nie istnieje, więc niech jej nie będzie
godzina dzwonka, start lekcjiTimeOnlypowtarza się codziennie, nie dotyczy jednego dnia
chwila zdarzenia — kiedy wypożyczono, kiedy zapisano plikDateTimepotrzebna data i godzina
ile trwa lekcja, ile dni spóźnieniaTimeSpanto odstęp, nie moment
chwila, która musi być jednoznaczna między strefamiDateTimeOffsetnosi 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ć:

Program.cs
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.

PRZYKŁAD

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 — pełny plik
// 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}";
    }
}
wynik w konsoli
=== 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. Termin to 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 DateTime spełnia umowę IComparable z lekcji 48, a metoda porównująca to jedna linia z CompareTo (lekcja 51).
  • DniDoTerminu odejmuje Termin.Date od dzisiaj.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 godzina jest przypisywana na nowo w każdym obiegu — bez tego nic by się nie przesuwało.
  • (lekcja + przerwa) * 4 - przerwa to 4 × 55 minus 10 = 210 minut. Wypisane przez Hours i Minutes daje „3 h 30 min”, a przez TotalMinutes — 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ło 12.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ą przez TryParseExact z InvariantCulture. Ten plik otworzy się poprawnie i w Polsce, i w Stanach.
  • Blok 7. proba.AddDays(30); bez przypisania nie zmieniło niczego. A AddMonths(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

  1. Zmień ustaloną datę na DateTime.Today i uruchom program. Wszystkie terminy przeliczą się na dzisiejszy dzień, a Ferdydurke będzie spóźniona o jeszcze więcej dni.
  2. W metodzie DniDoTerminu usuń oba .Date i 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.
  3. W bloku 3 usuń przypisanie godzina = koniec + przerwa;. Wszystkie cztery lekcje zaczną się o 08:00, bo DateTime nie zmienia się sam.
  4. 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.
  5. 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ł.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementWynikDziałanie i ograniczenia
DateTime.NowDateTimeData i godzina z zegara, czas lokalny.
DateTime.TodayDateTimeDzisiejsza data z godziną 00:00:00.
DateTime.UtcNowDateTimeCzas uniwersalny — do zapisywania chwil.
new DateTime(r, m, d)DateTimeKolejność od największej jednostki. Zła data daje ArgumentOutOfRangeException.
.Year, .Month, .DayintSkładniki daty.
.Hour, .Minute, .SecondintSkładniki godziny.
.DayOfWeekDayOfWeekTyp wyliczeniowy; Sunday ma wartość 0.
.DayOfYearintKtóry to dzień roku, 1–366.
.DateDateTimeSama data, godzina ustawiona na 00:00. Do porównywania dni.
.TimeOfDayTimeSpanSama godzina jako odstęp od północy.
DateTime.DaysInMonth(r, m)intDługość miesiąca, z uwzględnieniem lat przestępnych.
DateTime.IsLeapYear(r)boolZna pełną regułę, także wyjątek dla lat podzielnych przez 100.
.AddDays(n), .AddHours(n)…DateTimeZwraca nową wartość — trzeba ją przypisać. n może być ujemne.
.AddMonths(n), .AddYears(n)DateTimePrzycina dzień do długości miesiąca docelowego.
data1 - data2TimeSpanOdstęp między chwilami; może być ujemny.
data + odstępDateTimeNowa chwila.
TimeSpan.FromMinutes(n)TimeSpanTakże FromDays, FromHours, FromSeconds.
.Days, .Hours, .MinutesintCzęści odstępu. Hours mieści się w 0–23.
.TotalDays, .TotalHours…doubleCałość w tej jednostce, z częścią ułamkową.
.ToString("dd.MM.yyyy")stringMM miesiąc, mm minuty — wielkość liter jest istotna.
.ToString("d", kultura)stringFormaty standardowe: d, D, t, T, g, G, s, o.
DateTime.TryParse(t, k, s, out d)boolWynik zależy od ustawień regionalnych.
DateTime.TryParseExact(…)boolFormat podany jawnie — do danych z pliku i sieci.
new DateOnly(r, m, d)DateOnlySama data, bez godziny. Od .NET 6.
.DayNumberintNumer dnia; różnica dwóch numerów to liczba dni.
new TimeOnly(g, m)TimeOnlySama godzina, bez daty. Od .NET 6.
DateOnly.FromDateTime(d)DateOnlyOdrzuca część godzinową. Jest też TimeOnly.FromDateTime.
.KindDateTimeKindLocal, Utc albo Unspecified.
.ToUniversalTime(), .ToLocalTime()DateTimePrzeliczenie między czasem lokalnym a uniwersalnym.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
Data przechowywana jako stringNie da się odjąć, posortować ani sprawdzić poprawności. Napis przyjmie 31.02.2026.
data.AddDays(30); bez przypisaniaKompiluje 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-godzinnegohh 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ątrzWyjątek przy pierwszym złym wpisie. Zawsze TryParse albo TryParseExact.
TryParse dla daty z plikuWynik 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.yyyyPlik nie da się jednoznacznie odczytać na innym komputerze. Do plików yyyy-MM-dd z InvariantCulture.
Porównywanie dni bez .DateDateTime.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 DaysDla 53,5 godziny Days daje 2, a TotalDays — 2,23. Pierwsze to część, drugie całość.
Liczenie wieku jako rokDzis - rokUrodzeniaDaje o rok za dużo, dopóki urodziny w tym roku nie minęły. Trzeba dopisać sprawdzenie.
Liczenie terminów w miesiącachAddMonths(1).AddMonths(-1) dla 31 stycznia daje 28 stycznia. Terminy licz w dniach.
Własny warunek rok % 4 == 0 na rok przestępnyNie uwzględnia wyjątku dla lat podzielnych przez 100. Użyj DateTime.IsLeapYear.
(int)d.DayOfWeek jako numer dnia tygodniaSunday 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

Zadania

ZAD 1Co wiemy o dacie★☆☆

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.

ZAD 2Ile dni do wydarzenia★☆☆

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.

ZAD 3Plan dnia z TimeSpan★★☆

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.

ZAD 4Siedem formatów i jedna pułapka★★☆

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.

ZAD 5Ta sama data, dwa znaczenia★★☆

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.

ZAD 6Terminarz biblioteki — pełny program★★★

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.

PODSUMOWANIE

Co trzeba zapamiętać

  • Data to typ, nie napis: DateTime umie odejmować, porównywać, sortować i odrzucać daty nieistniejące.
  • new DateTime(rok, miesiąc, dzień) — kolejność zawsze od największej jednostki.
  • DateTime jest niezmienną strukturą, więc każda metoda Add… zwraca nową wartość do przypisania.
  • AddMonths przycina dzień do długości miesiąca, więc terminy licz w dniach.
  • Różnica dwóch dat to TimeSpan; data plus TimeSpan to znowu data.
  • Days to część odstępu, TotalDays to całość w dniach — to dwie różne liczby.
  • Porównując dni, używaj .Date, bo DateTime.Now nosi ze sobą godzinę.
  • W formatach MM to miesiąc, mm to minuty, HH to godzina 0–23, a hh — 1–12.
  • Format dla człowieka i format do pliku to dwie różne rzeczy: do pliku yyyy-MM-dd z InvariantCulture.
  • TryParse czyta według ustawień komputera, TryParseExact według podanego formatu — do plików tylko to drugie.
  • 03/04/2026 to 3 kwietnia albo 4 marca; bez jawnego formatu nie da się tego rozstrzygnąć.
  • DateOnly do dat bez godziny, TimeOnly do godzin bez daty, TimeSpan do odstępów.
  • Różnicę dni dla DateOnly policzysz, odejmując wartości DayNumber.
  • 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.