Przejdź do treści

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łęduPrzykładUdział
jedna zła cyfra1234 → 1734~79%
zamiana sąsiednich cyfr1234 → 1243~10%
inne (dalsze przestawienia, bliźniaki…)1224 → 1334reszta

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 dokumentacji

Wagi 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ółek

Dlaczego 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ści

Podwojenie 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 standardu

Duż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ą 1234 od 1243; 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.