Project P063

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.

2 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-14

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 messages
  • checks.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]

Built on these corpus cases

ΣEML 2026

Efficient New Language — a high-density semantic overlay for humans and AI agents.

This site’s demos run on the real EML toolchain, in your browser.

© 2026 EveMissLab (一言諾科技有限公司) / Neo.K. All rights reserved.