Formatowanie daty w C# — DateTime i CultureInfo

Formatowanie dat w C# to temat, który pozornie wygląda prosto — ale w produkcji potrafi zaskoczyć. Wystarczy pomylić MM z mm, polegać na kulturze maszyny serwera albo zapomnieć o strefach czasowych — i masz buga, który pojawia się tylko w nocy albo tylko na maszynach z innym locale.
W tym wpisie omawiam wszystko, czego potrzebujesz: specyfikatory standardowe i niestandardowe, CultureInfo, DateTimeOffset, DateOnly, parsowanie dat oraz wzorce, które stosuje się w realnych projektach .NET.
📌 Przykłady bazują na dacie:
DateTime dt = new DateTime(2026, 6, 7, 14, 30, 5);— 7 czerwca 2026, godzina 14:30:05.
1. Standardowe specyfikatory formatu daty
Jeden znak = predefiniowany format. Wynik zależy od kultury bieżącego wątku (Thread.CurrentThread.CurrentCulture).
DateTime dt = new DateTime(2026, 6, 7, 14, 30, 5);
Console.WriteLine(dt.ToString("d")); // 07.06.2026 (krótka data, pl-PL)
Console.WriteLine(dt.ToString("D")); // niedziela, 7 czerwca 2026
Console.WriteLine(dt.ToString("t")); // 14:30 (krótki czas)
Console.WriteLine(dt.ToString("T")); // 14:30:05 (pełny czas)
Console.WriteLine(dt.ToString("f")); // niedziela, 7 czerwca 2026 14:30
Console.WriteLine(dt.ToString("F")); // niedziela, 7 czerwca 2026 14:30:05
Console.WriteLine(dt.ToString("g")); // 07.06.2026 14:30
Console.WriteLine(dt.ToString("G")); // 07.06.2026 14:30:05
Console.WriteLine(dt.ToString("M")); // 7 czerwca (miesiąc i dzień)
Console.WriteLine(dt.ToString("Y")); // czerwiec 2026 (miesiąc i rok)
Console.WriteLine(dt.ToString("R")); // Sun, 07 Jun 2026 14:30:05 GMT (RFC 1123)
Console.WriteLine(dt.ToString("s")); // 2026-06-07T14:30:05 (sortowalne, invariant)
Console.WriteLine(dt.ToString("u")); // 2026-06-07 14:30:05Z (universal sortowalne)
Console.WriteLine(dt.ToString("o")); // 2026-06-07T14:30:05.0000000 (round-trip ISO 8601)Tabela — najczęściej używane specyfikatory
| Specyfikator | Wynik (pl-PL) | Kiedy używać |
|---|---|---|
"d" | 07.06.2026 | Wyświetlanie daty w UI |
"D" | niedziela, 7 czerwca 2026 | Nagłówki, komunikaty dla użytkownika |
"T" | 14:30:05 | Logi, UI z pełnym czasem |
"s" | 2026-06-07T14:30:05 | Sortowalne, niezależne od kultury |
"o" | 2026-06-07T14:30:05.0000000 | Serializacja, round-trip, JSON API |
"R" | Sun, 07 Jun 2026 14:30:05 GMT | Nagłówki HTTP (If-Modified-Since) |
2. Niestandardowe specyfikatory — budujesz format sam
Gdy żaden standardowy format Ci nie odpowiada, składasz go z liter-cegiełek:
DateTime dt = new DateTime(2026, 6, 7, 14, 30, 5);
Console.WriteLine(dt.ToString("yyyy-MM-dd")); // 2026-06-07
Console.WriteLine(dt.ToString("dd.MM.yyyy")); // 07.06.2026
Console.WriteLine(dt.ToString("dd.MM.yyyy HH:mm")); // 07.06.2026 14:30
Console.WriteLine(dt.ToString("dd.MM.yyyy HH:mm:ss")); // 07.06.2026 14:30:05
Console.WriteLine(dt.ToString("dddd, d MMMM yyyy")); // niedziela, 7 czerwca 2026
Console.WriteLine(dt.ToString("HH:mm:ss")); // 14:30:05
Console.WriteLine(dt.ToString("HH:mm")); // 14:30
Console.WriteLine(dt.ToString("d MMM yyyy")); // 7 cze 2026
Console.WriteLine(dt.ToString("yyyyMMdd_HHmmss")); // 20260607_143005 (nazwy plików)Tabela cegiełek niestandardowych
| Cegiełka | Znaczenie | Przykład |
|---|---|---|
yyyy | Rok 4-cyfrowy | 2026 |
yy | Rok 2-cyfrowy | 26 |
MM | Miesiąc (01–12) | 06 |
M | Miesiąc bez zera wiodącego | 6 |
MMMM | Pełna nazwa miesiąca | czerwiec |
MMM | Skrócona nazwa miesiąca | cze |
dd | Dzień (01–31) | 07 |
d | Dzień bez zera wiodącego | 7 |
dddd | Pełna nazwa dnia | niedziela |
HH | Godzina 24h (00–23) | 14 |
hh | Godzina 12h (01–12) | 02 |
mm | Minuta (00–59) | 30 |
ss | Sekunda (00–59) | 05 |
fff | Milisekundy (3 cyfry) | 000 |
tt | AM/PM | PM |
zzz | Offset strefy czasowej | +02:00 |
⚠️ Pułapka: MM (miesiąc) vs mm (minuta)
To najczęstszy błąd przy ręcznym budowaniu formatów. Wielkość liter ma znaczenie.
DateTime dt = new DateTime(2026, 6, 7, 14, 30, 5);
// ✅ Poprawnie — MM to miesiąc
Console.WriteLine(dt.ToString("dd.MM.yyyy")); // 07.06.2026
// ❌ Błąd — mm to MINUTA, nie miesiąc!
Console.WriteLine(dt.ToString("dd.mm.yyyy")); // 07.30.2026 ← buga w dacie
// Inne mylące pary:
// HH = godzina 24h vs hh = godzina 12h
// dddd = nazwa dnia vs d = numer dnia
// MMMM = pełna nazwa vs MM = numer miesiąca💡 Zapamiętaj: miesiąc to WIELKIE M, minuta to małe m. Godzina 24h to WIELKIE H, godzina 12h to małe h.
3. Formatowanie z CultureInfo — nie ufaj maszynie
Wynik ToString("D") bez podania kultury zależy od ustawień maszyny, na której działa kod. Na serwerze produkcyjnym może być inaczej niż na Twoim lokalnym środowisku.
using System.Globalization;
DateTime dt = new DateTime(2026, 6, 7, 14, 30, 5);
// Konkretna kultura — wynik przewidywalny niezależnie od maszyny
Console.WriteLine(dt.ToString("D", new CultureInfo("pl-PL")));
// niedziela, 7 czerwca 2026
Console.WriteLine(dt.ToString("D", new CultureInfo("en-US")));
// Sunday, June 7, 2026
Console.WriteLine(dt.ToString("D", new CultureInfo("de-DE")));
// Sonntag, 7. Juni 2026
// Invariant — ASCII, niezależne od kultury (do API, plików, logów)
Console.WriteLine(dt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
// 2026-06-07Kiedy używać InvariantCulture?
- Zawsze przy zapisywaniu daty do bazy danych, pliku CSV, logu lub odpowiedzi API.
- Zawsze gdy data jest odczytywana i interpretowana maszynowo (nie wyświetlana użytkownikowi).
- Nie przy wyświetlaniu daty w UI — tam chcesz kulturę użytkownika.
4. Formatowanie dat w interpolacji stringów
Możesz podać specyfikator formatu bezpośrednio w interpolacji — wynik jest taki sam jak ToString(format):
DateTime dt = new DateTime(2026, 6, 7, 14, 30, 5);
string display = $"Data: {dt:dd.MM.yyyy}"; // Data: 07.06.2026
string iso = $"ISO: {dt:yyyy-MM-dd}"; // ISO: 2026-06-07
string full = $"Zamówienie z {dt:D}"; // Zamówienie z niedziela, 7 czerwca 2026
string logEntry = $"[{dt:yyyy-MM-dd HH:mm:ss}] INFO"; // [2026-06-07 14:30:05] INFO⚠️ Interpolacja używa kultury bieżącego wątku — tak samo jak
ToString(format)bezCultureInfo. Dla API/pliku użyjFormattableString.Invariant($"{dt:yyyy-MM-dd}")lubdt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture).
5. Parsowanie dat — ParseExact i TryParseExact
Formatowanie to jedna strona medalu. Równie ważne jest parsowanie stringa z powrotem do DateTime. Unikaj DateTime.Parse() dla niezaufanych danych — rzuca wyjątek jeśli format nie pasuje. Zamiast tego:
using System.Globalization;
// TryParseExact — bezpieczna wersja (nie rzuca wyjątku)
string input = "07.06.2026";
bool success = DateTime.TryParseExact(
input,
"dd.MM.yyyy",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out DateTime result);
if (success)
Console.WriteLine(result); // 07.06.2026 00:00:00
// Kilka obsługiwanych formatów naraz:
string[] formats = { "dd.MM.yyyy", "yyyy-MM-dd", "d/M/yyyy" };
DateTime.TryParseExact(
"2026-06-07",
formats,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out DateTime result2); // 07.06.2026 00:00:00💡 Zasada:
ParseExactgdy format jest znany i kontrolowany (wewnętrzny system).TryParseExactdla danych od użytkownika lub zewnętrznych systemów.DateTime.Parsetylko dla prototypów i skryptów.
6. DateTimeOffset — gdy strefa czasowa ma znaczenie
DateTime nie przechowuje informacji o strefie czasowej. Jeśli Twoja aplikacja działa globalnie lub obsługuje użytkowników z różnych stref — użyj DateTimeOffset.
DateTimeOffset now = DateTimeOffset.UtcNow;
Console.WriteLine(now.ToString("o"));
// 2026-06-07T12:30:05.1234567+00:00 (UTC, round-trip)
DateTimeOffset local = DateTimeOffset.Now;
Console.WriteLine(local.ToString("o"));
// 2026-06-07T14:30:05.1234567+02:00 (czas lokalny z offsetem)
// Konwersja do strefy czasowej użytkownika:
var warsawZone = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time");
DateTimeOffset warsawTime = TimeZoneInfo.ConvertTime(now, warsawZone);
Console.WriteLine(warsawTime.ToString("dd.MM.yyyy HH:mm zzz"));
// 07.06.2026 14:30 +02:007. DateOnly i TimeOnly — C# 10+ (.NET 6+)
W wielu przypadkach nie potrzebujesz pełnego DateTime — chcesz tylko datę albo tylko czas. Od .NET 6 masz do tego dedykowane typy:
// DateOnly — tylko data, bez godziny
DateOnly date = new DateOnly(2026, 6, 7);
Console.WriteLine(date.ToString("yyyy-MM-dd")); // 2026-06-07
Console.WriteLine(date.ToString("d", new CultureInfo("pl-PL"))); // 07.06.2026
Console.WriteLine(date.AddDays(30)); // 07/07/2026
// TimeOnly — tylko czas, bez daty
TimeOnly time = new TimeOnly(14, 30, 5);
Console.WriteLine(time.ToString("HH:mm")); // 14:30
Console.WriteLine(time.ToString("HH:mm:ss")); // 14:30:05
Console.WriteLine(time.AddHours(2)); // 16:30
// Konwersja z/do DateTime:
DateTime dt = new DateTime(2026, 6, 7, 14, 30, 5);
DateOnly dateOnly = DateOnly.FromDateTime(dt); // 07.06.2026
TimeOnly timeOnly = TimeOnly.FromDateTime(dt); // 14:30:05Kiedy używać: data urodzenia, harmonogram wizyt (bez godziny), godziny otwarcia (bez daty), EF Core mapowanie kolumn date i time w SQL Server.
8. Wzorce produkcyjne — UTC w środku, lokalny na zewnątrz
Najczęściej stosowany wzorzec w produkcyjnych systemach .NET:
// ✅ PRZECHOWUJ w UTC — niezależne od strefy serwera i użytkownika
public class Order
{
public DateTime CreatedAtUtc { get; init; } = DateTime.UtcNow; // zawsze UTC
// lub:
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
}
// ✅ SERIALIZACJA do JSON/API — ISO 8601, InvariantCulture
public string ToIso(DateTime utcDate)
=> utcDate.ToString("o", CultureInfo.InvariantCulture);
// "2026-06-07T12:30:05.0000000"
// ✅ WYŚWIETLANIE użytkownikowi — kultura użytkownika
public string FormatForUser(DateTime utcDate, string userTimeZoneId, CultureInfo userCulture)
{
var tz = TimeZoneInfo.FindSystemTimeZoneById(userTimeZoneId);
var local = TimeZoneInfo.ConvertTimeFromUtc(utcDate, tz);
return local.ToString("D", userCulture);
}
// Dla Warszawy, pl-PL: "niedziela, 7 czerwca 2026"
// ✅ LOGI — sortowalne, invariant, z milisekundami
public string FormatForLog(DateTime utcDate)
=> utcDate.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture);
// "2026-06-07T14:30:05.123Z"Najczęstsze błędy
❌ Błąd 1: mm zamiast MM w dacie
// ❌ Zwróci minutę zamiast miesiąca!
dt.ToString("dd.mm.yyyy"); // "07.30.2026" — buga
// ✅
dt.ToString("dd.MM.yyyy"); // "07.06.2026"❌ Błąd 2: Poleganie na kulturze maszyny w API
// ❌ Wynik zależy od kultury serwera — nieprzewidywalny
var date = DateTime.UtcNow.ToString("D");
// ✅ Zawsze podaj kulturę dla API/pliku/bazy
var date = DateTime.UtcNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);❌ Błąd 3: DateTime.Now zamiast UtcNow w serwisach
// ❌ Czas lokalny serwera — zależy od konfiguracji, zmienia się przy DST
public class OrderService
{
public DateTime GetTimestamp() => DateTime.Now;
// ✅ UTC — niezmienny, porównywalny, niezależny od serwera
public DateTime GetTimestamp() => DateTime.UtcNow;
}❌ Błąd 4: DateTime.Parse dla niezaufanych danych
// ❌ Rzuci FormatException jeśli format nie pasuje
DateTime dt = DateTime.Parse(userInput);
// ✅ TryParseExact — nie rzuca, możesz obsłużyć błąd
if (!DateTime.TryParseExact(userInput, "dd.MM.yyyy",
CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime dt))
{
return BadRequest("Nieprawidłowy format daty. Wymagany: dd.MM.yyyy");
}Ściąga — szybki wybór
| Scenariusz | Rozwiązanie | Przykład wyniku |
|---|---|---|
| Data dla użytkownika (pl-PL) | dt.ToString("d", new CultureInfo("pl-PL")) | 07.06.2026 |
| Pełna data słowna | dt.ToString("D", new CultureInfo("pl-PL")) | niedziela, 7 czerwca 2026 |
| ISO 8601 dla API | dt.ToString("o", CultureInfo.InvariantCulture) | 2026-06-07T14:30:05.0000000 |
| Prosta data dla API | dt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture) | 2026-06-07 |
| Log (sortowalny) | dt.ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture) | 2026-06-07T14:30:05.123Z |
| Nazwa pliku | dt.ToString("yyyyMMdd_HHmmss") | 20260607_143005 |
| Czas w nagłówku HTTP | dt.ToString("R") | Sun, 07 Jun 2026 14:30:05 GMT |
| Parsowanie niezaufanego wejścia | DateTime.TryParseExact(str, format, ...) | — |
Podsumowanie
- MM = miesiąc, mm = minuta — ta pułapka kosztuje devów godziny debugowania.
- Przechowuj UTC (
DateTime.UtcNowlubDateTimeOffset.UtcNow), wyświetlaj w lokalu użytkownika. - API i pliki — zawsze
CultureInfo.InvariantCulturelub format"o"/"s". - UI — podawaj kulturę użytkownika jawnie, nie polegaj na kulturze maszyny.
- Parsowanie —
TryParseExactdla niezaufanych danych wejściowych. - DateOnly/TimeOnly (C# 10+) gdy potrzebujesz tylko daty albo tylko czasu — typ mówi więcej niż komentarz.
Zobacz także — powiązane artykuły
🚀 Co dalej?
Zobacz to w praktyce na wideo i pobierz darmową roadmapę, żeby ułożyć naukę w spójną ścieżkę do pierwszej pracy.
- 🗺️ Pobierz darmową roadmapę Junior .NET Developer — 12 kroków od podstaw C# do pierwszej pracy: dev-hobby.pl
- 🎬 Subskrybuj kanał YouTube — nowe filmy co tydzień.
Zamień wiedzę w umiejętności
Pobierz darmową Roadmapę .NET i ułóż takie tematy jak ten w spójną ścieżkę do pierwszej pracy.
Pobieram roadmapę →