Check digits
Luhn, ISBN-10 and ISBN-13 / EAN-13 check digits: check a number, add the check digit to one without, and test a valid number against every single wrong digit and every swap of neighbouring digits to see which slips each scheme catches - and which it lets through.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
Three check-digit schemes: Luhn, used on card and phone-equipment numbers, ISBN-10, and ISBN-13 / EAN-13, the barcode number on books and goods. You can check a number, add the check digit to a number without one, or test a valid number against every single wrong digit and every swap of two neighbouring digits, to see which slips the scheme catches and which it lets through. A check digit guards against slips of the hand, not against anyone faking a number.
main.eml- the menu, choosing a scheme, reading digits, and the messageschecks.eml- the three weighted sums, the check digit, and the error test
How each part works:
- Luhn: as in the corpus case
luhn-algorithm, start from the right and double every second digit, taking 9 off a result above 9; the number is valid when the total is a multiple of 10. - ISBN-10: the digits weigh 10, 9, ... down to 1 from the left, and the total must be a multiple of 11. The check digit can need the value 10, which is written X.
- ISBN-13 / EAN-13: the digits weigh 1, 3, 1, 3, ... from the left, and the total must be a multiple of 10.
- The check digit is the one last digit that makes the number valid; since the last digit weighs 1, exactly one does.
- The error test changes the number every possible way that a typing slip would: each digit to each other digit, and each pair of different neighbours swapped. It counts what the check catches. - All three catch every single wrong digit. - Only ISBN-10 also catches every swap, because 11 is prime. - Luhn lets a 09 / 90 swap through: doubled, 0 stays 0 and 9 becomes 18, which counts as 9, so both orders add up to 9. - EAN-13 lets a swap of two digits 5 apart through: with weights 1 and 3, a swap changes the total by twice the difference of the two digits, and twice 5 is 10.
What is checked: menu choices 1 to 4; schemes 1 to 3; digits, with spaces and hyphens skipped. An X is allowed only as an ISBN-10's last digit. The lengths are 2 to 19 digits for Luhn, 10 for ISBN-10 and 13 for EAN-13, each one fewer before the check digit is added. An error test starts from a valid number. An empty answer cancels.
Sessions: sessions/basic.in gives: - the published examples 79927398713 (Luhn), 0-306-40615-2 (ISBN-10) and 978-0-306-40615-7 (ISBN-13), each checked with its sum; - check digits added: X for 155404295, 1 for 400638133393, and 7 for 40903; - error tests: - 409037 under Luhn misses its 09 and 90 swaps; - 4006381333931 under EAN-13 misses the 38 swap; - 0306406152 under ISBN-10 catches every swap.
sessions/bad-input.in gives: - menu choices 0 and x, and scheme 4; - ISBN-10 input 12345, abc, and 15540429X5 with an X that is not last; - 79927398710, which is not valid (its check digit should be 3), checked and then error-tested; - a 5-digit EAN-13 body.
Built on the verified corpus case luhn-algorithm (doubling every second digit from the right, the total a multiple of 10).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal== Check digits ==
Luhn, ISBN-10 and ISBN-13 / EAN-13: the last digit catches most slips of the hand.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 0
Pick a number from 1 to 4.
1) check a number 2) add the check digit 3) error test 4) quit
choice> x
Pick a number from 1 to 4.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 1
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 4
Type 1, 2 or 3.
scheme>
Cancelled.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 1
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 2
digits (spaces and hyphens are fine)> 12345
An ISBN-10 number has 10 digits; that one has 5.
digits (spaces and hyphens are fine)> abc
Use digits only, with an X allowed as the last one.
digits (spaces and hyphens are fine)> 15540429X5
Use digits only, with an X allowed as the last one.
digits (spaces and hyphens are fine)>
Cancelled.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 1
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 1
digits (spaces and hyphens are fine)> 79927398710
79927398710 is not valid: its sum is 67, which leaves 7 over 10. With these first digits the check digit would be 3.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 3
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 1
digits (spaces and hyphens are fine)> 79927398710
Start from a valid number - use 2) to add its check digit.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 2
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 3
digits (spaces and hyphens are fine)> 12345
An ISBN-13 / EAN-13 number has 12 digits before its check digit; that one has 5.
digits (spaces and hyphens are fine)>
Cancelled.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 4
Bye.
What was typed (22 lines)
0
x
1
4
1
2
12345
abc
15540429X5
1
1
79927398710
3
1
79927398710
2
3
12345
4
basic
interpreter: byte-equal== Check digits ==
Luhn, ISBN-10 and ISBN-13 / EAN-13: the last digit catches most slips of the hand.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 1
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 1
digits (spaces and hyphens are fine)> 79927398713
79927398713 is valid: its Luhn sum is 70, a multiple of 10.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 1
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 2
digits (spaces and hyphens are fine)> 0-306-40615-2
0306406152 is valid: its ISBN-10 sum is 132, a multiple of 11.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 1
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 3
digits (spaces and hyphens are fine)> 978-0-306-40615-7
9780306406157 is valid: its ISBN-13 / EAN-13 sum is 100, a multiple of 10.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 2
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 2
digits (spaces and hyphens are fine)> 155404295
The check digit is X: 155404295X.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 2
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 3
digits (spaces and hyphens are fine)> 400638133393
The check digit is 1: 4006381333931.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 2
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 1
digits (spaces and hyphens are fine)> 40903
The check digit is 7: 409037.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 3
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 1
digits (spaces and hyphens are fine)> 409037
One wrong digit: 54 of the 54 ways are caught.
Two neighbouring digits swapped: 3 of the 5 swaps are caught.
not caught: 09 -> 90 at digits 2 and 3
not caught: 90 -> 09 at digits 3 and 4
1) check a number 2) add the check digit 3) error test 4) quit
choice> 3
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 3
digits (spaces and hyphens are fine)> 4006381333931
One wrong digit: 117 of the 117 ways are caught.
Two neighbouring digits swapped: 8 of the 9 swaps are caught.
not caught: 38 -> 83 at digits 5 and 6
1) check a number 2) add the check digit 3) error test 4) quit
choice> 3
1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13
scheme> 2
digits (spaces and hyphens are fine)> 0306406152
One wrong digit: 91 of the 91 ways are caught.
Two neighbouring digits swapped: 9 of the 9 swaps are caught.
1) check a number 2) add the check digit 3) error test 4) quit
choice> 4
Bye.
What was typed (28 lines)
1
1
79927398713
1
2
0-306-40615-2
1
3
978-0-306-40615-7
2
2
155404295
2
3
400638133393
2
1
40903
3
1
409037
3
3
4006381333931
3
2
0306406152
4
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
eml# P063 check digits: check a number against Luhn, ISBN-10 or ISBN-13 /
# EAN-13, add the check digit to a number without one, and test which
# typing errors each scheme would catch on a given number. A check digit
# guards against slips of the hand, not against anyone faking a number.
import checks
def trim(s):
0 => i
len(s) => j
while i < j and s[i] == " ":
i + 1 => i
while j > i and s[j - 1] == " ":
j - 1 => j
return s[i:j]
def ask_scheme():
" 1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13" ^0
while True:
trim(input("scheme> ")) => answer
if answer == "":
return -1
if answer == "1" or answer == "2" or answer == "3":
return int(answer) - 1
"Type 1, 2 or 3." ^0
def ask_digits(scheme, with_check):
# The number's digits, or [] when the answer is empty.
checks.lengths(scheme) => span
span[0] => low
span[1] => high
if not with_check:
low - 1 => low
high - 1 => high
while True:
trim(input("digits (spaces and hyphens are fine)> ")) => answer
if answer == "":
return []
checks.digits_of(scheme, answer, with_check) => ds
if len(ds) == 0:
if scheme == 1 and with_check:
"Use digits only, with an X allowed as the last one." ^0
else:
"Use digits only." ^0
elif len(ds) < low or len(ds) > high:
if low == high and with_check:
("An " + checks.names()[scheme] + " number has " + str(low) + " digits; that one has " + str(len(ds)) + ".") ^0
elif low == high:
("An " + checks.names()[scheme] + " number has " + str(low) + " digits before its check digit; that one has " + str(len(ds)) + ".") ^0
else:
("Type " + str(low) + " to " + str(high) + " digits; that one has " + str(len(ds)) + ".") ^0
else:
return ds
def check():
ask_scheme() => scheme
if scheme == -1:
"Cancelled." ^0
return
ask_digits(scheme, True) => ds
if len(ds) == 0:
"Cancelled." ^0
return
checks.weighted_total(scheme, ds) => total
checks.modulus(scheme) => m
if checks.valid(scheme, ds):
(checks.as_text(ds) + " is valid: its " + checks.names()[scheme] + " sum is " + str(total) + ", a multiple of " + str(m) + ".") ^0
else:
len(ds) => n
checks.check_digit(scheme, ds[0:n - 1]) => want
(checks.as_text(ds) + " is not valid: its sum is " + str(total) + ", which leaves " + str(total % m) + " over " + str(m) + ". With these first digits the check digit would be " + checks.digit_text(want) + ".") ^0
def complete():
ask_scheme() => scheme
if scheme == -1:
"Cancelled." ^0
return
ask_digits(scheme, False) => body
if len(body) == 0:
"Cancelled." ^0
return
checks.check_digit(scheme, body) => d
("The check digit is " + checks.digit_text(d) + ": " + checks.as_text(body + [d]) + ".") ^0
def test_errors():
ask_scheme() => scheme
if scheme == -1:
"Cancelled." ^0
return
ask_digits(scheme, True) => ds
if len(ds) == 0:
"Cancelled." ^0
return
if not checks.valid(scheme, ds):
"Start from a valid number - use 2) to add its check digit." ^0
return
checks.error_test(scheme, ds) => r
("One wrong digit: " + str(r[0]) + " of the " + str(r[1]) + " ways are caught.") ^0
("Two neighbouring digits swapped: " + str(r[2]) + " of the " + str(r[3]) + " swaps are caught.") ^0
for miss in r[4]:
(" not caught: " + checks.digit_text(miss[1]) + checks.digit_text(miss[2]) + " -> " + checks.digit_text(miss[2]) + checks.digit_text(miss[1]) + " at digits " + str(miss[0] + 1) + " and " + str(miss[0] + 2)) ^0
"== Check digits ==" ^0
"Luhn, ISBN-10 and ISBN-13 / EAN-13: the last digit catches most slips of the hand." ^0
True => running
while running:
"" ^0
"1) check a number 2) add the check digit 3) error test 4) quit" ^0
trim(input("choice> ")) => choice
if choice == "1":
check()
elif choice == "2":
complete()
elif choice == "3":
test_errors()
elif choice == "4":
False => running
else:
"Pick a number from 1 to 4." ^0
"Bye." ^0
Python projection (main.py)
import checks
def trim(s):
i = 0
j = len(s)
while i < j and s[i] == " ":
i = i + 1
while j > i and s[j - 1] == " ":
j = j - 1
return s[i:j]
def ask_scheme():
print(" 1) Luhn 2) ISBN-10 3) ISBN-13 / EAN-13")
while True:
answer = trim(input("scheme> "))
if answer == "":
return -1
if answer == "1" or answer == "2" or answer == "3":
return int(answer) - 1
print("Type 1, 2 or 3.")
def ask_digits(scheme, with_check):
span = checks.lengths(scheme)
low = span[0]
high = span[1]
if not with_check:
low = low - 1
high = high - 1
while True:
answer = trim(input("digits (spaces and hyphens are fine)> "))
if answer == "":
return []
ds = checks.digits_of(scheme, answer, with_check)
if len(ds) == 0:
if scheme == 1 and with_check:
print("Use digits only, with an X allowed as the last one.")
else:
print("Use digits only.")
elif len(ds) < low or len(ds) > high:
if low == high and with_check:
print("An " + checks.names()[scheme] + " number has " + str(low) + " digits; that one has " + str(len(ds)) + ".")
elif low == high:
print("An " + checks.names()[scheme] + " number has " + str(low) + " digits before its check digit; that one has " + str(len(ds)) + ".")
else:
print("Type " + str(low) + " to " + str(high) + " digits; that one has " + str(len(ds)) + ".")
else:
return ds
def check():
scheme = ask_scheme()
if scheme == -1:
print("Cancelled.")
return
ds = ask_digits(scheme, True)
if len(ds) == 0:
print("Cancelled.")
return
total = checks.weighted_total(scheme, ds)
m = checks.modulus(scheme)
if checks.valid(scheme, ds):
print(checks.as_text(ds) + " is valid: its " + checks.names()[scheme] + " sum is " + str(total) + ", a multiple of " + str(m) + ".")
else:
n = len(ds)
want = checks.check_digit(scheme, ds[0:n - 1])
print(checks.as_text(ds) + " is not valid: its sum is " + str(total) + ", which leaves " + str(total % m) + " over " + str(m) + ". With these first digits the check digit would be " + checks.digit_text(want) + ".")
def complete():
scheme = ask_scheme()
if scheme == -1:
print("Cancelled.")
return
body = ask_digits(scheme, False)
if len(body) == 0:
print("Cancelled.")
return
d = checks.check_digit(scheme, body)
print("The check digit is " + checks.digit_text(d) + ": " + checks.as_text(body + [d]) + ".")
def test_errors():
scheme = ask_scheme()
if scheme == -1:
print("Cancelled.")
return
ds = ask_digits(scheme, True)
if len(ds) == 0:
print("Cancelled.")
return
if not checks.valid(scheme, ds):
print("Start from a valid number - use 2) to add its check digit.")
return
r = checks.error_test(scheme, ds)
print("One wrong digit: " + str(r[0]) + " of the " + str(r[1]) + " ways are caught.")
print("Two neighbouring digits swapped: " + str(r[2]) + " of the " + str(r[3]) + " swaps are caught.")
for miss in r[4]:
print(" not caught: " + checks.digit_text(miss[1]) + checks.digit_text(miss[2]) + " -> " + checks.digit_text(miss[2]) + checks.digit_text(miss[1]) + " at digits " + str(miss[0] + 1) + " and " + str(miss[0] + 2))
print("== Check digits ==")
print("Luhn, ISBN-10 and ISBN-13 / EAN-13: the last digit catches most slips of the hand.")
running = True
while running:
print("")
print("1) check a number 2) add the check digit 3) error test 4) quit")
choice = trim(input("choice> "))
if choice == "1":
check()
elif choice == "2":
complete()
elif choice == "3":
test_errors()
elif choice == "4":
running = False
else:
print("Pick a number from 1 to 4.")
print("Bye.")
checks.eml
eml# P063 check digits - three schemes. A number is a list of digits 0 to 9,
# with 10 standing for the X an ISBN-10 may end in. Each scheme adds a last
# digit so that a weighted sum comes out a multiple of 10 (or 11), and a
# typing error usually breaks that.
def names():
return ["Luhn", "ISBN-10", "ISBN-13 / EAN-13"]
def lengths(scheme):
# [shortest, longest] number, check digit included.
if scheme == 0:
return [2, 19]
if scheme == 1:
return [10, 10]
return [13, 13]
def luhn_total(ds):
# The corpus case luhn-algorithm: from the right, every second digit is
# doubled, 9 taken off when that passes 9, and everything added up.
0 => total
len(ds) => n
for i in [0:n - 1]:
ds[n - 1 - i] => d
if i % 2 == 1:
d * 2 => d
if d > 9:
d - 9 => d
total + d => total
return total
def weighted_total(scheme, ds):
if scheme == 0:
return luhn_total(ds)
0 => total
if scheme == 1:
# ISBN-10: weights 10, 9, ..., 1 from the left.
for i in [0:9]:
total + (10 - i) * ds[i] => total
return total
# EAN-13: weights 1, 3, 1, 3, ... from the left.
for i in [0:12]:
1 => w
if i % 2 == 1:
3 => w
total + w * ds[i] => total
return total
def modulus(scheme):
if scheme == 1:
return 11
return 10
def valid(scheme, ds):
return weighted_total(scheme, ds) % modulus(scheme) == 0
def check_digit(scheme, body):
# The last digit that makes body valid: try each, 0 to 9 (and X, 10,
# for ISBN-10). Exactly one works, since the last digit's weight is 1.
10 => top
if scheme == 1:
11 => top
for d in [0:top - 1]:
if valid(scheme, body + [d]):
return d
return -1
def digit_text(d):
if d == 10:
return "X"
return str(d)
def as_text(ds):
"" => s
for d in ds:
s + digit_text(d) => s
return s
def digits_of(scheme, s, with_check):
# The digits of s, spaces and hyphens skipped, or [] when anything else
# is there. An X (either case) is allowed only as an ISBN-10's check
# digit.
[] => out
"" => kept
for ch in s:
if ch != " " and ch != "-":
kept + ch => kept
for i in [0:len(kept) - 1]:
kept[i] => ch
if ch >= "0" and ch <= "9":
out + [int(ch)] => out
elif (ch == "X" or ch == "x") and scheme == 1 and with_check and i == len(kept) - 1:
out + [10] => out
else:
return []
return out
def error_test(scheme, ds):
# Every single wrong digit and every swap of two neighbouring different
# digits, and how many of each the check catches. Returns [singles
# caught, singles, swaps caught, swaps, the swaps that slip through].
0 => s_caught
0 => s_all
len(ds) => n
for i in [0:n - 1]:
9 => top
if scheme == 1 and i == n - 1:
10 => top
for d in [0:top]:
if d != ds[i]:
ds[0:n] => e
d => e[i]
s_all + 1 => s_all
if not valid(scheme, e):
s_caught + 1 => s_caught
0 => t_caught
0 => t_all
[] => missed
for i in [0:n - 2]:
if ds[i] != ds[i + 1]:
ds[0:n] => e
ds[i + 1] => e[i]
ds[i] => e[i + 1]
# an X can only stand last
if not (scheme == 1 and e[n - 2] == 10):
t_all + 1 => t_all
if valid(scheme, e):
missed + [[i, ds[i], ds[i + 1]]] => missed
else:
t_caught + 1 => t_caught
return [s_caught, s_all, t_caught, t_all, missed]
Python projection (checks.py)
def names():
return ["Luhn", "ISBN-10", "ISBN-13 / EAN-13"]
def lengths(scheme):
if scheme == 0:
return [2, 19]
if scheme == 1:
return [10, 10]
return [13, 13]
def luhn_total(ds):
total = 0
n = len(ds)
for i in range(0, n):
d = ds[n - 1 - i]
if i % 2 == 1:
d = d * 2
if d > 9:
d = d - 9
total = total + d
return total
def weighted_total(scheme, ds):
if scheme == 0:
return luhn_total(ds)
total = 0
if scheme == 1:
for i in range(0, 10):
total = total + (10 - i) * ds[i]
return total
for i in range(0, 13):
w = 1
if i % 2 == 1:
w = 3
total = total + w * ds[i]
return total
def modulus(scheme):
if scheme == 1:
return 11
return 10
def valid(scheme, ds):
return weighted_total(scheme, ds) % modulus(scheme) == 0
def check_digit(scheme, body):
top = 10
if scheme == 1:
top = 11
for d in range(0, top):
if valid(scheme, body + [d]):
return d
return -1
def digit_text(d):
if d == 10:
return "X"
return str(d)
def as_text(ds):
s = ""
for d in ds:
s = s + digit_text(d)
return s
def digits_of(scheme, s, with_check):
out = []
kept = ""
for ch in s:
if ch != " " and ch != "-":
kept = kept + ch
for i in range(0, len(kept)):
ch = kept[i]
if ch >= "0" and ch <= "9":
out = out + [int(ch)]
elif (ch == "X" or ch == "x") and scheme == 1 and with_check and i == len(kept) - 1:
out = out + [10]
else:
return []
return out
def error_test(scheme, ds):
s_caught = 0
s_all = 0
n = len(ds)
for i in range(0, n):
top = 9
if scheme == 1 and i == n - 1:
top = 10
for d in range(0, top+1):
if d != ds[i]:
e = ds[0:n]
e[i] = d
s_all = s_all + 1
if not valid(scheme, e):
s_caught = s_caught + 1
t_caught = 0
t_all = 0
missed = []
for i in range(0, n - 2+1):
if ds[i] != ds[i + 1]:
e = ds[0:n]
e[i] = ds[i + 1]
e[i + 1] = ds[i]
if not (scheme == 1 and e[n - 2] == 10):
t_all = t_all + 1
if valid(scheme, e):
missed = missed + [[i, ds[i], ds[i + 1]]]
else:
t_caught = t_caught + 1
return [s_caught, s_all, t_caught, t_all, missed]