Skip to content

Repository files navigation

Check Digit Atlas

Luhn misses 100% of jump transpositions. Swap any two digits that sit one apart on a bank card number and the checksum guarding every card payment on earth is mathematically incapable of noticing. Not most of the time — every time.

This repo measures that, and nine other schemes, by brute force.

Read the atlas →

What it does

For ten real check-digit schemes, generate valid identifiers and inject every classical transcription error at every position, then count how many still validate. 3,800,668 mutations in total.

error type what it is share of real errors*
single one character wrong ~79%
adjacent swap two neighbouring characters exchanged ~10%
jump swap characters two apart exchanged ~1%
twin a doubled pair both changed the same way ~0.5%

* from Verhoeff's 1969 study of transcription errors, which is also where the taxonomy comes from.

Results

scheme single adjacent jump twin
Luhn — bank cards, IMEI 0 2.10% 100% 6.36%
ISO 6346 — shipping containers 5.74% 3.85% 2.95% 3.96%
EAN-13 — barcodes, ISBN-13 0 11.25% 100% 11.11%
ISBN-10 0 0 0 11.16%
IBAN 0 0 0 0
VIN 7.48% 8.51% 7.20% 19.39%
NHS number 0 0 0 10.74%
Australian TFN 0 0 0 0
Verhoeff 0 0 6.18% 4.62%
Damm 0 0 10.04% 8.88%

Three things worth pulling out:

  • Luhn and EAN-13 miss every jump transposition. Their weights alternate, so two positions one apart share a weight parity and the sum is unchanged. Structural, not statistical — and confirmed exhaustively rather than sampled.
  • IBAN and the Australian tax file number were the only schemes that let nothing through. Both beat the number on your bank card.
  • VIN is worst on single characters. Its transliteration maps several letters onto one value — A and J both count as 1 — so substituting one for another cannot change the checksum.

Luhn's adjacent-swap blind spot is exactly 09, confirmed across all 90 ordered digit pairs.

Reproducing

python3 test_schemes.py   # pin every scheme to its published test vectors
python3 measure.py        # ~30s, writes results.json
python3 build_page.py     # regenerate index.html from atlas.html

No dependencies. Python 3.8+.

schemes.py implements each scheme from its published definition. test_schemes.py pins all ten to published test vectors — the Luhn classic, GB82WEST12345698765432, a VIN with an X check character, Verhoeff 2363, Damm 5724 — and it runs before anything is measured, because the whole value here is that the numbers are right. checkdigit.js is the browser port, cross-checked against the Python on identical inputs; the two agree on all 40 scheme × error-type cells.

What this does not show

Miss rates are measured over generated identifiers, not real-world ones, and real identifiers are not uniformly distributed — card numbers carry issuer prefixes, containers carry owner codes. Rates for the structured schemes would shift somewhat on real corpora. The 100% jump figures would not: those follow from the weighting.

Multi-error mistakes are out of scope, as is anything about fraud. A check digit catches typos, never forgery.

Every identifier shown is randomly generated. None is a real card, account, patient or vehicle.

Licence

MIT.

About

Luhn misses 100% of jump transpositions. Ten check-digit schemes, 3.8 million injected typos, measured not asserted.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages