Cyfry kontrolne – jak PESEL, NIP, IBAN i numer karty łapią literówki
Numery, które nosisz w portfelu – PESEL, NIP, numer karty płatniczej, konto bankowe – mają wspólny sekret: ich ostatnia cyfra nie niesie żadnej informacji. Jest wyliczona z pozostałych po to, żeby formularz mógł wykryć literówkę, zanim dane w ogóle dotrą do serwera. To zastosowana na co dzień arytmetyka modularna – ta sama, która w wiecznym kalendarzu liczyła dni tygodnia. Zobaczmy, jak projektuje się taką cyfrę, dlaczego różne numery używają różnych wzorów i co te wzory potrafią, a czego nie.
Jakie błędy popełnia człowiek
Suma kontrolna jest bronią wycelowaną w konkretnego przeciwnika: człowieka przepisującego numer. Badania nad błędami transkrypcji (klasyczna praca J. Verhoeffa z 1969 r.) pokazują wyraźny rozkład:
| Typ błędu | Przykład | Udział |
|---|---|---|
| jedna zła cyfra | 1234 → 1734 | ~79% |
| zamiana sąsiednich cyfr | 1234 → 1243 | ~10% |
| inne (dalsze przestawienia, bliźniaki…) | 1224 → 1334 | reszta |
Dobra cyfra kontrolna musi łapać przede wszystkim te dwa pierwsze przypadki. I tu pojawia się subtelność: zwykła suma cyfr nie wykrywa przestawień – 1234 i 1243 sumują się identycznie. Dlatego prawie każdy schemat mnoży cyfry przez wagi zależne od pozycji: po przestawieniu sąsiadów iloczyny się zmieniają i suma przestaje się zgadzać.
PESEL: wagi i modulo 10
PESEL ma 11 cyfr: sześć koduje datę urodzenia, cztery to liczba porządkowa (z płcią), a ostatnia jest kontrolna. Pierwsze dziesięć cyfr mnoży się przez wagi 1, 3, 7, 9, 1, 3, 7, 9, 1, 3, a cyfra kontrolna dopełnia sumę do pełnej dziesiątki:
def is_valid_pesel(pesel: str) -> bool:
if len(pesel) != 11 or not pesel.isdigit():
return False
weights = (1, 3, 7, 9, 1, 3, 7, 9, 1, 3)
checksum = sum(int(d) * w for d, w in zip(pesel, weights))
return (10 - checksum % 10) % 10 == int(pesel[10])
is_valid_pesel("44051401359") # True – przykładowy PESEL z dokumentacjiWagi 1, 3, 7, 9 nie są przypadkowe: wszystkie są względnie pierwsze z 10, więc zmiana dowolnej pojedynczej cyfry zawsze zmienia sumę modulo 10 – każdy błąd jednej cyfry zostanie wykryty. Przestawienia sąsiadów łapią się wtedy, gdy sąsiednie wagi są różne, czyli prawie zawsze (schemat przepuści tylko nieliczne konfiguracje).
PESEL to też mini-kalendarz. Sześć pierwszych cyfr to data w formacie RRMMDD, ale miesiąc koduje również stulecie: urodzeni po 1999 r. mają do miesiąca dodane 20 (styczeń 2004 = „24"), urodzeni w XIX w. – 80. Dzięki temu 11 cyfr obsłuży daty aż do 2299 roku. Przedostatnia cyfra koduje płeć: parzysta – kobieta, nieparzysta – mężczyzna. W przykładzie wyżej: 44 05 14 → 14 maja 1944, cyfra 5 → mężczyzna. Więcej o strukturze: PESEL w Wikipedii.
NIP: modulo 11, czyli po co moduł pierwszy
NIP ma 10 cyfr; dziewięć pierwszych mnoży się przez wagi 6, 5, 7, 2, 3, 4, 5, 6, 7, a suma modulo 11 musi się równać ostatniej cyfrze:
<?php
function isValidNip(string $nip): bool
{
$nip = preg_replace('/[^0-9]/', '', $nip); // "774-00-01-454" też przejdzie
if (strlen($nip) !== 10) {
return false;
}
$weights = [6, 5, 7, 2, 3, 4, 5, 6, 7];
$sum = 0;
foreach ($weights as $i => $w) {
$sum += $w * (int) $nip[$i];
}
return $sum % 11 === (int) $nip[9];
}
isValidNip('774-00-01-454'); // true – NIP jednej z największych polskich spółekDlaczego 11, skoro cyfry są dziesiętne? Bo 11 jest liczbą pierwszą – a wtedy matematyka gwarantuje więcej: przy różnych wagach sąsiednich schemat modulo 11 wykrywa wszystkie błędy pojedynczej cyfry i wszystkie przestawienia sąsiadów. Cena tej elegancji: suma modulo 11 może wyjść 10, którego nie da się zapisać jedną cyfrą. Stary ISBN-10 rozwiązywał to znakiem „X” na końcu numeru; w Polsce prościej – NIP-ów z resztą 10 po prostu się nie wydaje. (Nowszy ISBN-13 wrócił do modulo 10 z wagami 1 i 3 – kosztem gwarancji na przestawienia – bo musiał być zgodny z kodami kreskowymi EAN.)
Numer karty: algorytm Luhna
Numer karty płatniczej sprawdza algorytm Luhna z 1954 roku – zaprojektowany tak, by dało się go policzyć prostym układem mechanicznym. Zamiast tablicy wag: idąc od prawej, podwajamy co drugą cyfrę, a jeśli wynik jest dwucyfrowy – sumujemy jego cyfry (co sprowadza się do odjęcia 9). Numer jest poprawny, gdy suma dzieli się przez 10:
function isValidLuhn(number) {
const digits = number.replace(/\D/g, ''); // spacje i myślniki precz
let sum = 0;
for (let i = 0; i < digits.length; i++) {
let d = +digits[digits.length - 1 - i]; // od prawej
if (i % 2 === 1) {
d *= 2; // co druga cyfra razy 2…
if (d > 9) d -= 9; // …a 14 liczy się jako 1 + 4 = 5
}
sum += d;
}
return digits.length > 1 && sum % 10 === 0;
}
isValidLuhn('4242 4242 4242 4242'); // true – numer testowy znany każdemu,
// kto integrował bramkę płatnościPodwojenie działa jak waga „2”, a redukcja −9 domyka sztuczkę: dzięki niej każda zmiana pojedynczej cyfry zmienia sumę. Luhn łapie 100% błędów jednej cyfry i wszystkie przestawienia sąsiadów z jednym wyjątkiem: pary 09 ↔ 90 (bo 9 i „podwojone 0” ważą tyle samo). To świadomy kompromis prostoty – i standard ISO/IEC 7812, którym do dziś posługują się wszystkie karty świata.
IBAN: dwie cyfry i modulo 97
Numer konta to najdłuższy numer, jaki przepisujemy – i najdotkliwszy w skutkach pomyłki. Dlatego IBAN używa najcięższej artylerii (standard ISO 7064): dwóch cyfr kontrolnych i modulo 97 – największej dwucyfrowej liczby pierwszej. Walidacja: przenieś cztery pierwsze znaki na koniec, zamień litery na liczby (A = 10, …, Z = 35) i sprawdź, czy całość modulo 97 daje 1:
def is_valid_iban(iban: str) -> bool:
iban = iban.replace(" ", "").upper()
rearranged = iban[4:] + iban[:4] # "PL61…" – kraj i kontrolne na koniec
digits = "".join(str(int(ch, 36)) for ch in rearranged) # A=10 … Z=35
remainder = 0
for d in digits: # modulo "strumieniowo", cyfra po cyfrze –
remainder = (remainder * 10 + int(d)) % 97 # bez arytmetyki wielkich liczb
return remainder == 1
is_valid_iban("PL61 1090 1014 0000 0712 1981 2874") # True – przykład ze standarduDuży moduł pierwszy i dwie cyfry kontrolne dają wykrywalność, o jakiej schematy jednocyfrowe mogą pomarzyć: wszystkie błędy pojedynczej cyfry, wszystkie przestawienia (także niesąsiadujących cyfr) i ~99,97% dowolnych losowych przekłamań. Warto też docenić sztuczkę w kodzie: 30-cyfrowej liczby nie trzeba nigdzie przechowywać – resztę z dzielenia liczymy przyrostowo, cyfra po cyfrze, dokładnie tak, jak uczy się dzielenia pisemnego.
Czego suma kontrolna NIE robi
Walidacja to nie weryfikacja. Cyfra kontrolna chroni przed literówką, nie przed kłamstwem – wzór jest jawny, więc każdy może wygenerować „poprawny" PESEL czy NIP, który nie należy do nikogo. Formularz z walidacją Luhna odsieje pomyłkę w numerze karty, ale nie sprawdzi, czy karta istnieje i ma środki. Suma kontrolna to ergonomia, nie bezpieczeństwo – tego drugiego szukaj w zupełnie innych wpisach.
Z tego samego powodu nie warto w formularzach robić z walidacji bramki nie do przejścia: numer idealnie poprawny formalnie może być cudzy, a zbyt gorliwa walidacja (np. odrzucanie spacji w IBAN-ie) bywa częstszym źródłem frustracji niż literówki, przed którymi chroni.
Podsumowanie
- Cyfra kontrolna to funkcja pozostałych cyfr – nadmiarowość dodana po to, by wykryć najczęstsze ludzkie błędy: pojedynczą złą cyfrę (~79%) i zamianę sąsiadów (~10%).
- Wagi pozycyjne odróżniają
1234od1243; wagi względnie pierwsze z modułem gwarantują wykrycie każdej pojedynczej pomyłki (PESEL: wagi 1‑3‑7‑9, mod 10). - Moduł pierwszy daje pełne gwarancje także dla przestawień – kosztem kłopotliwej reszty 10 (NIP: mod 11; ISBN-10 z „X” na końcu).
- Luhn (karty płatnicze) osiąga prawie to samo bez tablicy wag – podwajaniem co drugiej cyfry; przepuszcza jedynie parę 09 ↔ 90.
- IBAN z dwiema cyframi kontrolnymi i mod 97 wykrywa praktycznie wszystko, a resztę z dzielenia 30-cyfrowej liczby liczy się strumieniowo, bez big-intów.
- Suma kontrolna to ochrona przed literówką, nie oszustwem – poprawny formalnie numer wciąż może nie istnieć.
A jeśli arytmetyka modularna przypadła Ci do gustu: to na niej stoi też wieczny kalendarz i data Wielkanocy – modulo 7 i modulo 30 zamiast modulo 10 i 97.