Tahle kapitola není o velkých konceptech, ale o malých, nudných věcech, co spolehlivě kousnou. Fungují v dev, projdou code review, a pak v produkci nasčítají špatné peníze, zobrazí blbý čas nebo rozbijí jméno s háčkem. Žádná z nich není těžká — ale junior na ně doplácí pořád dokola. Tady je přehled těch nejčastějších, ať je znáš dřív, než tě potkají.
Čas a časová pásma — past číslo jedna
Čas je zákeřnější, než vypadá. Pravidla, která ti ušetří hodiny debugování:
- Ukládej a počítej vždy v UTC. Časové pásmo řeš až při zobrazení uživateli. Když uložíš „lokální čas", časem nevíš, v jakém pásmu byl, a všechno se rozjede.
- Nikdy nevěř hodinám klienta — jsou rozsynchronizované (clock skew z Distribuovaných). Autoritativní čas drží server.
- Letní/zimní čas (DST) dělá zmatek — některé hodiny v roce neexistují, jiné se opakují. UTC tenhle problém nemá (proto v něm ukládáš).
- Datum bez času (narozeniny) je něco jiného než okamžik — nepřeváděj ho přes časová pásma.
Peníze — NIKDY ne float
Zlaté pravidlo: peníze nikdy neukládej ani nepočítej jako desetinné číslo (float). Float neumí
přesně reprezentovat třeba 0.1, takže ti při sčítání vznikají chyby v haléřích — a u peněz je
chyba o haléř fatální (a nasčítá se).
- Ukládej peníze jako celá čísla v nejmenší jednotce (haléře/centy), nebo ještě líp jako přesný decimal typ — ten zvládne i různý počet desetinných míst.
- ⚠️ Počet desetinných míst závisí na měně: koruna/dolar mají 2, ale jen (JPY) má 0 a dinár (BHD, KWD) má 3 — natvrdo „×100" se proto rozbije. A u jednotkových cen (cena za litr) potřebuješ klidně víc desetin než u koncové částky.
- Pozor i na zaokrouhlování (kdy a jak) a na to, že částka bez měny nedává smysl (hodnota = číslo + měna, viz value object v Architektuře).
Text, encoding a Unicode
- Používej všude UTF-8. Když se někde míchá jiné kódování, háčky a čárky se změní v „Äìå", emoji v otazníky.
- Délka řetězce neznamená počet písmen. Emoji nebo znak s háčkem může „zabrat" víc bajtů či jednotek — naivní ořез textu uprostřed takového znaku ho rozbije.
- Normalizace. Stejně vypadající znak může být uložený dvěma způsoby (např. „é" jako jeden znak, nebo „e" + háček). Proto se text před porovnáváním normalizuje (souvisí to s vyhledáváním).
Null, prázdno a chybějící hodnota
null≠ prázdný řetězec ≠ chybějící klíč ≠ nula. „Nezadané telefonní číslo" (null) je něco jiného než „prázdné" a než „číslo 0". Měj jasno, co která situace znamená.- Null se šíří. Operace s null často vrátí null nebo spadne — ošetři ho na hranicích (vstup, databáze), ať ti „neprosákne" do míst, kde ho nečekáš.
Pár dalších klasik
- Off-by-one — chyba o jedničku (
<vs<=, indexace od nuly). Hlídej hranice 0, prázdno, max. - Časová zóna v cronu — naplánovaná úloha „ve 2:00" v jakém pásmu? (Viz Background Jobs.)
- Lokálně to jede — různé prostředí (verze, locale, kódování) → testuj v podmínkách blízkých produkci.
Failure modes — jak to v praxi praská
- Lokální čas v databázi → po nasazení do jiného pásma (nebo po DST) jsou všechny časy špatně.
- Peníze ve floatu → zůstatky a faktury nesedí o haléře, které se nasčítají.
- Smíchané kódování → rozbitá jména a texty, „padají" znaky.
- Záměna null / prázdno / nula → špatná logika (např. „0 Kč sleva" se chová jako „žádná sleva").
🛠️ Cvičení
- Oprav čas. Máš v databázi sloupec s lokálním časem objednávky. Vysvětli, co se rozbije po nasazení do jiného pásma, a jak to opravit.
- Spočítej cenu. Ukaž (klidně čísly), jak sečtení
0.1 + 0.2ve floatu nedá přesně0.3, a navrhni správné uložení peněz. - Ořež text. Máš zkrátit jméno na 10 znaků. Co se může pokazit u jména s emoji nebo háčky a jak to udělat bezpečně?
- Null vs prázdno. Pro pole „telefon" rozliš tři stavy (null / prázdno / vyplněno) a co každý znamená v aplikaci.
- Najdi off-by-one. Stránkuješ po 20 a vracíš položky od indexu
offsetdooffset + 20včetně obou konců. Kolik položek tím vrátíš a kde je ta chyba o jedničku?
Náčrt řešení — rozbal, až si cvičení zkusíš sám
- Oprav čas — lokální čas nenese pásmo, takže po nasazení do jiného pásma (nebo po DST) jsou všechny časy posunuté a nevíš, jak je převést; oprava je ukládat a počítat v UTC a do místního pásma převádět až při zobrazení. Pozor: datum bez času (narozeniny) přes pásma nepřeváděj.
- Spočítej cenu —
0.1 + 0.2ve floatu vyjde0.30000…04, protože float neumí přesně reprezentovat desetinná čísla; ukládej peníze jako celá čísla v nejmenší jednotce (haléře/centy) nebo jako decimal. Pozor: počet desetin závisí na měně (JPY 0, BHD/KWD 3), takže natvrdo „×100" se rozbije, a částka musí nést i měnu. - Ořež text — naivní ořez na 10 „znaků" může rozseknout emoji nebo písmeno s háčkem uprostřed (zabírají víc jednotek/bajtů) a rozbít ho; ořezávej po celých znacích s ohledem na Unicode. Pozor: drž všude UTF-8 a počítej s tím, že délka řetězce ≠ počet písmen.
- Null vs prázdno — null = telefon nevyplněn (chybí), prázdný řetězec = vyplněno prázdně, vyplněná hodnota = konkrétní číslo; každý stav znamená v aplikaci něco jiného. Pozor: nemíchej je (např. „0 sleva" vs „žádná sleva") a ošetři null na hranicích, ať neprosákne dál.
- Najdi off-by-one — od
offsetdooffset + 20včetně obou konců vrátí 21 položek, ne 20; chyba o jedničku je v tom „včetně obou konců" (<=místo<). Pozor: hlídej hranice u indexace od nuly a stránkování po dvaceti.
🧠 Otázky & odpovědi
Proč ukládat čas v UTC a pásmo řešit až při zobrazení?
Protože „lokální čas" sám o sobě nenese informaci, v jakém pásmu byl — časem nevíš, jak ho převést, a po nasazení do jiného pásma nebo po přechodu na letní čas se všechno rozjede. UTC je jeden univerzální základ bez DST problémů: ukládáš a počítáš v něm a do místního času převedeš až ve chvíli, kdy ho ukazuješ konkrétnímu uživateli.
Proč se peníze nesmí ukládat jako float?
Protože float neumí přesně reprezentovat běžná desetinná čísla (např. 0.1), takže při sčítání vznikají
drobné chyby — a u peněz je chyba o haléř fatální a navíc se nasčítá. Správně se peníze ukládají jako
celá čísla v nejmenší jednotce (haléře/centy) nebo jako přesný decimal typ. A částka by měla
vždy nést i měnu.
Proč délka řetězce neznamená počet písmen?
Protože některé znaky (emoji, písmena s háčky) zaberou víc než jednu „jednotku" — naivní length může
vrátit víc, než kolik vidíš písmen, a naivní ořez textu může rozseknout znak uprostřed a rozbít ho.
Proto u textu počítej s Unicode, drž všude UTF-8 a ořezávej opatrně (po celých znacích).
Jaký je rozdíl mezi null, prázdným řetězcem a nulou?
Jsou to tři různé věci. null = hodnota chybí / není zadaná (telefon, který uživatel nevyplnil). Prázdný řetězec = je tam hodnota, ale prázdná. 0 = konkrétní číslo nula. Když je smícháš (např. sleva: null = žádná vs 0 = nulová), dostaneš špatnou logiku. Měj jasno, co každý stav znamená, a ošetři null na hranicích, ať ti neprosákne dál.
Proč argument u mě to jede nestačí jako důkaz, že kód funguje?
Protože tvoje prostředí se liší od produkce — jiná verze runtime, jiné locale a kódování, jiné časové pásmo, jiná data. Spousta těchhle correctness pastí (čas, encoding, zaokrouhlení) se projeví až tam, kde je nastavení jiné. Proto se testuje v podmínkách co nejblíž produkci (stejné verze, UTC, UTF-8) a kritické věci (čas, peníze) se ošetřují explicitně, ne že se spolehneš na default prostředí.
