Project P101

IBAN checker

International bank account numbers checked the ISO 13616 way, step by step: letters as numbers and the remainder mod 97 taken nine digits at a time; check digits made from an account part, leading zeros kept; and every IBAN one typing mistake away that would pass.

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

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

International bank account numbers, checked the ISO 13616 way and shown step by step. You can also make an IBAN from a country and an account part, and see what a single typing mistake does to one.

  • main.eml - the menu, the step-by-step check, making an IBAN, the typo search and the country list
  • iban.eml - cleaning the input, the country lengths, letters as numbers, the remainder piece by piece, the check digits and the one-change neighbours

How each part works:

  • The check: move the first four characters (country and check digits) to the end and turn each letter into two digits (A is 10, ..., Z is 35). The resulting number must leave 1 when divided by 97.
  • That number is long: 28 digits for the British example. As in the corpus case iban-mod97-validator, it is taken mod 97 a piece at a time. The remainder so far goes in front of the next digits, and no piece is longer than 9 digits. Each piece is shown, and the whole number's remainder is printed beside it as a second route.
  • Making an IBAN: the check digits are 98 minus the remainder of the account part, the country and 00.
  • An IBAN is text, never a number. As in the corpus case leading-zero-identifiers, an account part that starts with zeros keeps them. The program says how many characters would be left if they were read as a number.
  • Only each country's length is checked, not the layout of its account part.
  • The typo search: for an IBAN that fails, every IBAN one change away that passes. A change is a digit replaced by a digit, a letter by a letter, or two neighbours swapped. A replaced character, or a swap of two digits or two letters, always changes the remainder. A letter swapped with a digit can keep it: in the account part that happens exactly for B and 1, M and 2, or X and 3, because B is 11, M is 22 and X is 33.

What is checked: menu choices 1 to 5; letters, digits and spaces only, with lower case raised; two letters for a country in the list; check digits that are digits; the country's length; an account part of the right length. An empty answer cancels.

Sessions: sessions/basic.in gives: - a valid British IBAN, step by step; - the same with its last digit changed, typed in lower case; - a German one a character short; - the German and Danish check digits made from their account parts (the Danish one keeps its two leading zeros); - a made-up British IBAN with B and 1 side by side, then the same with the two swapped, which passes too; - the typo search on an IBAN with its last two digits swapped: 7 IBANs one change away pass, the right one among them; - the typo search on a valid IBAN, and the country list.

sessions/bad-input.in gives: - menu choices 0 and x; - an IBAN with a hyphen, a cancel, one too short, one without a country, an unknown country, and check digits that are letters; - the country Germany, an account part of 3 characters, two cancels; - the typo search on an IBAN of the wrong length, and on one with a stray symbol; a cancel.

Built on the verified corpus cases iban-mod97-validator (mod 97 on a number too big to hold, two ways) and leading-zero-identifiers (identifiers made of digits are not numbers).

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
== IBAN checker ==
An IBAN is valid when, with its first four characters moved to the end and its letters turned into numbers, it leaves 1 when divided by 97.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 0
Pick a number from 1 to 5.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> x
Pick a number from 1 to 5.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 1
IBAN> GB82-WEST
Type letters and digits only; spaces are fine.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 1
IBAN> 
Cancelled.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 1
IBAN> 1234
1234
Not valid: too short to be an IBAN.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 1
IBAN> 12AB 3456 7890
12AB 3456 7890
Not valid: an IBAN starts with two letters, the country.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 1
IBAN> XX12 3456 7890
XX12 3456 7890
Not valid: XX is not a country in this list.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 1
IBAN> GBAB WEST 1234 5698 7654 32
GBAB WEST 1234 5698 7654 32
  country GB: 22 characters; this one has 22.
Not valid: the third and fourth characters, the check digits, must be digits.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 2
country (two letters)> Germany
Type a country from the list, such as DE or GB.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 2
country (two letters)> DE
account part (18 letters and digits)> 123
A DE account part has 18 characters, letters and digits, spaces left out.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 2
country (two letters)> 
Cancelled.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 2
country (two letters)> DE
account part (18 letters and digits)> 
Cancelled.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 3
an IBAN that fails> DE89 3704
Not that kind of failure: DE IBANs have 22 characters, this has 8.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 3
an IBAN that fails> x!
Type letters and digits only; spaces are fine.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 3
an IBAN that fails> 
Cancelled.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 5
Bye.
What was typed (31 lines)
0
x
1
GB82-WEST
1

1
1234
1
12AB 3456 7890
1
XX12 3456 7890
1
GBAB WEST 1234 5698 7654 32
2
Germany
2
DE
123
2

2
DE

3
DE89 3704
3
x!
3

5

basic

interpreter: byte-equal
== IBAN checker ==
An IBAN is valid when, with its first four characters moved to the end and its letters turned into numbers, it leaves 1 when divided by 97.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 1
IBAN> GB82 WEST 1234 5698 7654 32
GB82 WEST 1234 5698 7654 32
  country GB: 22 characters; this one has 22.
  first four to the end: WEST12345698765432GB82
  letters as numbers:    3214282912345698765432161182 (28 digits)
  mod 97, 9 digits at a time:
    321428291 mod 97 = 70
    702345698 mod 97 = 29
    297654321 mod 97 = 24
    2461182 mod 97 = 1
  the whole number mod 97 = 1, the same.
Valid: mod 97 is 1.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 1
IBAN> gb82 west 1234 5698 7654 33
GB82 WEST 1234 5698 7654 33
  country GB: 22 characters; this one has 22.
  first four to the end: WEST12345698765433GB82
  letters as numbers:    3214282912345698765433161182 (28 digits)
  mod 97, 9 digits at a time:
    321428291 mod 97 = 70
    702345698 mod 97 = 29
    297654331 mod 97 = 34
    3461182 mod 97 = 28
  the whole number mod 97 = 28, the same.
Not valid: mod 97 is not 1, so a character is wrong.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 1
IBAN> DE89 3704 0044 0532 0130 0
DE89 3704 0044 0532 0130 0
  country DE: 22 characters; this one has 21.
Not valid: DE IBANs have 22 characters, this has 21.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 2
country (two letters)> DE
account part (18 letters and digits)> 370400440532013000
Check digits: 98 - (370400440532013000DE00, letters as numbers, mod 97) = 89.
DE89 3704 0044 0532 0130 00
  country DE: 22 characters; this one has 22.
  first four to the end: 370400440532013000DE89
  letters as numbers:    370400440532013000131489 (24 digits)
  mod 97, 9 digits at a time:
    370400440 mod 97 = 23
    235320130 mod 97 = 70
    700013148 mod 97 = 38
    389 mod 97 = 1
  the whole number mod 97 = 1, the same.
Valid: mod 97 is 1.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 2
country (two letters)> dk
account part (14 letters and digits)> 0040 0440 1162 43
Check digits: 98 - (00400440116243DK00, letters as numbers, mod 97) = 50.
DK50 0040 0440 1162 43
  country DK: 18 characters; this one has 18.
  first four to the end: 00400440116243DK50
  letters as numbers:    00400440116243132050 (20 digits)
  mod 97, 9 digits at a time:
    004004401 mod 97 = 47
    471624313 mod 97 = 31
    312050 mod 97 = 1
  the whole number mod 97 = 1, the same.
Valid: mod 97 is 1.
The account part starts with 2 zeros. Kept as text they stay; read as a number they would be lost, leaving 12 characters where DK needs 14.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 2
country (two letters)> GB
account part (18 letters and digits)> ABCB 1234 5678 9012 34
Check digits: 98 - (ABCB12345678901234GB00, letters as numbers, mod 97) = 60.
GB60 ABCB 1234 5678 9012 34
  country GB: 22 characters; this one has 22.
  first four to the end: ABCB12345678901234GB60
  letters as numbers:    1011121112345678901234161160 (28 digits)
  mod 97, 9 digits at a time:
    101112111 mod 97 = 87
    872345678 mod 97 = 40
    409012341 mod 97 = 7
    761160 mod 97 = 1
  the whole number mod 97 = 1, the same.
Valid: mod 97 is 1.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 1
IBAN> GB60 ABC1 B234 5678 9012 34
GB60 ABC1 B234 5678 9012 34
  country GB: 22 characters; this one has 22.
  first four to the end: ABC1B2345678901234GB60
  letters as numbers:    1011121112345678901234161160 (28 digits)
  mod 97, 9 digits at a time:
    101112111 mod 97 = 87
    872345678 mod 97 = 40
    409012341 mod 97 = 7
    761160 mod 97 = 1
  the whole number mod 97 = 1, the same.
Valid: mod 97 is 1.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 3
an IBAN that fails> GB82 WEST 1234 5698 7654 23
One character changed: 5 IBANs pass. Two neighbours swapped: 2 pass.
  replace  GB82 WPST 1234 5698 7654 23
  replace  GB82 WESV 1234 5698 7654 23
  replace  GB82 WEST 1834 5698 7654 23
  replace  GB82 WEST 1234 5698 7754 23
  replace  GB82 WEST 1234 5698 7657 23
  swap     GB82 WETS 1234 5698 7654 23
  swap     GB82 WEST 1234 5698 7654 32
Mod 97 catches the mistake but cannot say which one it was: each of these is a valid IBAN.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 3
an IBAN that fails> GB60 ABCB 1234 5678 9012 34
That one is valid. A wrong digit in place of a digit, or a wrong letter in place of a letter, always changes the remainder; so does swapping two different digits, or two different letters, next to each other.
A letter swapped with a digit next to it in the account part slips through when the two are B and 1, M and 2, or X and 3: B is 11, M is 22 and X is 33.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 4
Countries and IBAN lengths: AT 20, BE 16, CH 21, DE 22, DK 18, ES 24, FI 18, FR 27, GB 22, IE 22, IT 27, LU 20, NL 18, NO 15, PL 28, PT 25, SE 24.
Only the length is checked for each country, not the layout of its account part.

1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit
choice> 5
Bye.
What was typed (23 lines)
1
GB82 WEST 1234 5698 7654 32
1
gb82 west 1234 5698 7654 33
1
DE89 3704 0044 0532 0130 0
2
DE
370400440532013000
2
dk
0040 0440 1162 43
2
GB
ABCB 1234 5678 9012 34
1
GB60 ABC1 B234 5678 9012 34
3
GB82 WEST 1234 5698 7654 23
3
GB60 ABCB 1234 5678 9012 34
4
5

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
# P101 IBAN checker: check an IBAN step by step, make one from a country
# and an account part, and see what one typing mistake does.
import iban

def plain(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_iban(prompt):
    # a cleaned IBAN, "" to cancel, or "!" after a complaint
    plain(input(prompt + "> ")) => t
    if t == "":
        return ""
    iban.clean(t) => s
    if s == "":
        "Type letters and digits only; spaces are fine." ^0
        return "!"
    return s

def show_check(s):
    iban.check(s) => r
    (iban.grouped(s)) ^0
    iban.countries() => lengths
    if len(s) >= 2 and s[0:2] in lengths:
        ("  country " + s[0:2] + ": " + str(lengths[s[0:2]]) + " characters; this one has " + str(len(s)) + ".") ^0
    if r[0] or r[1] == "mod 97 is not 1, so a character is wrong":
        iban.rearranged(s) => moved
        iban.expand(moved) => digits
        ("  first four to the end: " + moved) ^0
        ("  letters as numbers:    " + digits + " (" + str(len(digits)) + " digits)") ^0
        iban.mod97_steps(digits) => m
        "  mod 97, 9 digits at a time:" ^0
        for st in m[1]:
            ("    " + st[0] + " mod 97 = " + str(st[1])) ^0
        ("  the whole number mod 97 = " + str(int(digits) % 97) + ", the same.") ^0
    if r[0]:
        "Valid: mod 97 is 1." ^0
    else:
        ("Not valid: " + r[1] + ".") ^0

def make():
    plain(input("country (two letters)> ")) => c
    if c == "":
        "Cancelled." ^0
        return
    iban.clean(c) => c
    iban.countries() => lengths
    if not (c in lengths):
        "Type a country from the list, such as DE or GB." ^0
        return
    lengths[c] - 4 => need
    plain(input("account part (" + str(need) + " letters and digits)> ")) => b
    if b == "":
        "Cancelled." ^0
        return
    iban.clean(b) => b
    if len(b) != need:
        ("A " + c + " account part has " + str(need) + " characters, letters and digits, spaces left out.") ^0
        return
    iban.check_digits(c, b) => d
    c + d + b => s
    ("Check digits: 98 - (" + b + c + "00, letters as numbers, mod 97) = " + d + ".") ^0
    show_check(s)
    0 => zeros
    while zeros < len(b) and b[zeros] == "0":
        zeros + 1 => zeros
    if zeros > 0:
        b[zeros:len(b)] => short
        ("The account part starts with " + str(zeros) + " zero" + plural(zeros) + ". Kept as text they stay; read as a number they would be lost, leaving " + str(len(short)) + " characters where " + c + " needs " + str(need) + ".") ^0

def plural(n):
    if n == 1:
        return ""
    return "s"

def look_for_typos():
    ask_iban("an IBAN that fails") => s
    if s == "":
        "Cancelled." ^0
        return
    if s == "!":
        return
    iban.check(s) => r
    if r[0]:
        "That one is valid. A wrong digit in place of a digit, or a wrong letter in place of a letter, always changes the remainder; so does swapping two different digits, or two different letters, next to each other." ^0
        "A letter swapped with a digit next to it in the account part slips through when the two are B and 1, M and 2, or X and 3: B is 11, M is 22 and X is 33." ^0
        return
    if r[1] != "mod 97 is not 1, so a character is wrong":
        ("Not that kind of failure: " + r[1] + ".") ^0
        return
    iban.typos(s) => found
    0 => n_rep
    0 => n_swap
    for f in found:
        if f[0] == "replace":
            n_rep + 1 => n_rep
        else:
            n_swap + 1 => n_swap
    ("One character changed: " + str(n_rep) + " IBAN" + plural(n_rep) + " pass. Two neighbours swapped: " + str(n_swap) + " pass" + verb(n_swap) + ".") ^0
    for f in found:
        "swap   " => kind
        if f[0] == "replace":
            "replace" => kind
        ("  " + kind + "  " + iban.grouped(f[1])) ^0
    "Mod 97 catches the mistake but cannot say which one it was: each of these is a valid IBAN." ^0

def verb(n):
    if n == 1:
        return "es"
    return ""

def show_countries():
    iban.countries() => lengths
    "" => line
    for c in ["AT", "BE", "CH", "DE", "DK", "ES", "FI", "FR", "GB", "IE", "IT", "LU", "NL", "NO", "PL", "PT", "SE"]:
        if line != "":
            line + ", " => line
        line + c + " " + str(lengths[c]) => line
    ("Countries and IBAN lengths: " + line + ".") ^0
    "Only the length is checked for each country, not the layout of its account part." ^0

"== IBAN checker ==" ^0
"An IBAN is valid when, with its first four characters moved to the end and its letters turned into numbers, it leaves 1 when divided by 97." ^0
True => running
while running:
    "" ^0
    "1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit" ^0
    plain(input("choice> ")) => choice
    if choice == "1":
        ask_iban("IBAN") => s
        if s == "":
            "Cancelled." ^0
        elif s != "!":
            show_check(s)
    elif choice == "2":
        make()
    elif choice == "3":
        look_for_typos()
    elif choice == "4":
        show_countries()
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
Python projection (main.py)
import iban

def plain(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_iban(prompt):
    t = plain(input(prompt + "> "))
    if t == "":
        return ""
    s = iban.clean(t)
    if s == "":
        print("Type letters and digits only; spaces are fine.")
        return "!"
    return s

def show_check(s):
    r = iban.check(s)
    print(iban.grouped(s))
    lengths = iban.countries()
    if len(s) >= 2 and s[0:2] in lengths:
        print("  country " + s[0:2] + ": " + str(lengths[s[0:2]]) + " characters; this one has " + str(len(s)) + ".")
    if r[0] or r[1] == "mod 97 is not 1, so a character is wrong":
        moved = iban.rearranged(s)
        digits = iban.expand(moved)
        print("  first four to the end: " + moved)
        print("  letters as numbers:    " + digits + " (" + str(len(digits)) + " digits)")
        m = iban.mod97_steps(digits)
        print("  mod 97, 9 digits at a time:")
        for st in m[1]:
            print("    " + st[0] + " mod 97 = " + str(st[1]))
        print("  the whole number mod 97 = " + str(int(digits) % 97) + ", the same.")
    if r[0]:
        print("Valid: mod 97 is 1.")
    else:
        print("Not valid: " + r[1] + ".")

def make():
    c = plain(input("country (two letters)> "))
    if c == "":
        print("Cancelled.")
        return
    c = iban.clean(c)
    lengths = iban.countries()
    if not c in lengths:
        print("Type a country from the list, such as DE or GB.")
        return
    need = lengths[c] - 4
    b = plain(input("account part (" + str(need) + " letters and digits)> "))
    if b == "":
        print("Cancelled.")
        return
    b = iban.clean(b)
    if len(b) != need:
        print("A " + c + " account part has " + str(need) + " characters, letters and digits, spaces left out.")
        return
    d = iban.check_digits(c, b)
    s = c + d + b
    print("Check digits: 98 - (" + b + c + "00, letters as numbers, mod 97) = " + d + ".")
    show_check(s)
    zeros = 0
    while zeros < len(b) and b[zeros] == "0":
        zeros = zeros + 1
    if zeros > 0:
        short = b[zeros:len(b)]
        print("The account part starts with " + str(zeros) + " zero" + plural(zeros) + ". Kept as text they stay; read as a number they would be lost, leaving " + str(len(short)) + " characters where " + c + " needs " + str(need) + ".")

def plural(n):
    if n == 1:
        return ""
    return "s"

def look_for_typos():
    s = ask_iban("an IBAN that fails")
    if s == "":
        print("Cancelled.")
        return
    if s == "!":
        return
    r = iban.check(s)
    if r[0]:
        print("That one is valid. A wrong digit in place of a digit, or a wrong letter in place of a letter, always changes the remainder; so does swapping two different digits, or two different letters, next to each other.")
        print("A letter swapped with a digit next to it in the account part slips through when the two are B and 1, M and 2, or X and 3: B is 11, M is 22 and X is 33.")
        return
    if r[1] != "mod 97 is not 1, so a character is wrong":
        print("Not that kind of failure: " + r[1] + ".")
        return
    found = iban.typos(s)
    n_rep = 0
    n_swap = 0
    for f in found:
        if f[0] == "replace":
            n_rep = n_rep + 1
        else:
            n_swap = n_swap + 1
    print("One character changed: " + str(n_rep) + " IBAN" + plural(n_rep) + " pass. Two neighbours swapped: " + str(n_swap) + " pass" + verb(n_swap) + ".")
    for f in found:
        kind = "swap   "
        if f[0] == "replace":
            kind = "replace"
        print("  " + kind + "  " + iban.grouped(f[1]))
    print("Mod 97 catches the mistake but cannot say which one it was: each of these is a valid IBAN.")

def verb(n):
    if n == 1:
        return "es"
    return ""

def show_countries():
    lengths = iban.countries()
    line = ""
    for c in ["AT", "BE", "CH", "DE", "DK", "ES", "FI", "FR", "GB", "IE", "IT", "LU", "NL", "NO", "PL", "PT", "SE"]:
        if line != "":
            line = line + ", "
        line = line + c + " " + str(lengths[c])
    print("Countries and IBAN lengths: " + line + ".")
    print("Only the length is checked for each country, not the layout of its account part.")

print("== IBAN checker ==")
print("An IBAN is valid when, with its first four characters moved to the end and its letters turned into numbers, it leaves 1 when divided by 97.")
running = True
while running:
    print("")
    print("1) check an IBAN  2) make one  3) look for a typo  4) countries  5) quit")
    choice = plain(input("choice> "))
    if choice == "1":
        s = ask_iban("IBAN")
        if s == "":
            print("Cancelled.")
        elif s != "!":
            show_check(s)
    elif choice == "2":
        make()
    elif choice == "3":
        look_for_typos()
    elif choice == "4":
        show_countries()
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")

iban.eml

eml
# P101 IBAN checker - the ISO 13616 rule: move the first four characters
# to the end, turn letters into numbers (A = 10 ... Z = 35), and the whole
# number mod 97 must be 1. The number has 30 or more digits; as in the
# corpus case iban-mod97-validator it is taken mod 97 a piece at a time,
# never holding more than 9 digits, and checked against the whole number.
# An IBAN is text, never a number: as in the corpus case
# leading-zero-identifiers, its leading zeros are part of it.

def countries():
    # length of a full IBAN, by country (only the length is checked, not
    # each country's own layout of the account part)
    return {"AT": 20, "BE": 16, "CH": 21, "DE": 22, "DK": 18, "ES": 24, "FI": 18, "FR": 27, "GB": 22, "IE": 22, "IT": 27, "LU": 20, "NL": 18, "NO": 15, "PL": 28, "PT": 25, "SE": 24}

def letter_values():
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => abc
    {} => out
    for i in [0:25]:
        10 + i => out[abc[i]]
    return out

def is_digit(c):
    return c >= "0" and c <= "9"

def is_letter(c):
    return c >= "A" and c <= "Z"

def clean(text):
    # spaces dropped, lower case raised; "" if any other character is left
    "abcdefghijklmnopqrstuvwxyz" => low
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => up
    {} => raise_
    for i in [0:25]:
        up[i] => raise_[low[i]]
    "" => out
    for c in text:
        if c != " ":
            if c in raise_:
                out + raise_[c] => out
            elif is_digit(c) or is_letter(c):
                out + c => out
            else:
                return ""
    return out

def grouped(s):
    "" => out
    for i in [0:len(s) - 1]:
        if i > 0 and i % 4 == 0:
            out + " " => out
        out + s[i] => out
    return out

def expand(s):
    # every letter as its two digits
    letter_values() => v
    "" => out
    for c in s:
        if is_letter(c):
            out + str(v[c]) => out
        else:
            out + c => out
    return out

def mod97_steps(digits):
    # [remainder, steps]: up to 9 digits at a time, the remainder so far
    # written in front of the next piece; each step is [piece, remainder]
    [] => steps
    "" => carry
    0 => i
    0 => r
    while i < len(digits):
        9 - len(carry) => take
        carry + digits[i:i + take] => piece
        int(piece) % 97 => r
        steps + [[piece, r]] => steps
        str(r) => carry
        i + take => i
    return [r, steps]

def rearranged(s):
    return s[4:len(s)] + s[0:4]

def check(s):
    # [valid, reason]; s is already clean
    countries() => lengths
    if len(s) < 5:
        return [False, "too short to be an IBAN"]
    if not (is_letter(s[0]) and is_letter(s[1])):
        return [False, "an IBAN starts with two letters, the country"]
    if not (s[0:2] in lengths):
        return [False, s[0:2] + " is not a country in this list"]
    if not (is_digit(s[2]) and is_digit(s[3])):
        return [False, "the third and fourth characters, the check digits, must be digits"]
    if len(s) != lengths[s[0:2]]:
        return [False, s[0:2] + " IBANs have " + str(lengths[s[0:2]]) + " characters, this has " + str(len(s))]
    if mod97_steps(expand(rearranged(s)))[0] != 1:
        return [False, "mod 97 is not 1, so a character is wrong"]
    return [True, "mod 97 is 1"]

def check_digits(country, bban):
    # the two digits that make country + digits + bban come out at 1
    mod97_steps(expand(bban + country + "00"))[0] => r
    98 - r => d
    if d < 10:
        return "0" + str(d)
    return str(d)

def typos(s):
    # [[kind, IBAN], ...]: every IBAN one change away from s that passes -
    # one character replaced by another of its kind, or two neighbours
    # swapped
    "0123456789" => digits
    "ABCDEFGHIJKLMNOPQRSTUVWXYZ" => letters
    [] => out
    for i in [0:len(s) - 1]:
        digits => pool
        if is_letter(s[i]):
            letters => pool
        for c in pool:
            if c != s[i]:
                s[0:i] + c + s[i + 1:len(s)] => t
                if check(t)[0]:
                    out + [["replace", t]] => out
    for i in [0:len(s) - 2]:
        if s[i] != s[i + 1]:
            s[0:i] + s[i + 1] + s[i] + s[i + 2:len(s)] => t
            if check(t)[0]:
                out + [["swap", t]] => out
    return out
Python projection (iban.py)
def countries():
    return {"AT": 20, "BE": 16, "CH": 21, "DE": 22, "DK": 18, "ES": 24, "FI": 18, "FR": 27, "GB": 22, "IE": 22, "IT": 27, "LU": 20, "NL": 18, "NO": 15, "PL": 28, "PT": 25, "SE": 24}

def letter_values():
    abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    out = {}
    for i in range(0, 26):
        out[abc[i]] = 10 + i
    return out

def is_digit(c):
    return c >= "0" and c <= "9"

def is_letter(c):
    return c >= "A" and c <= "Z"

def clean(text):
    low = "abcdefghijklmnopqrstuvwxyz"
    up = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    raise_ = {}
    for i in range(0, 26):
        raise_[low[i]] = up[i]
    out = ""
    for c in text:
        if c != " ":
            if c in raise_:
                out = out + raise_[c]
            elif is_digit(c) or is_letter(c):
                out = out + c
            else:
                return ""
    return out

def grouped(s):
    out = ""
    for i in range(0, len(s)):
        if i > 0 and i % 4 == 0:
            out = out + " "
        out = out + s[i]
    return out

def expand(s):
    v = letter_values()
    out = ""
    for c in s:
        if is_letter(c):
            out = out + str(v[c])
        else:
            out = out + c
    return out

def mod97_steps(digits):
    steps = []
    carry = ""
    i = 0
    r = 0
    while i < len(digits):
        take = 9 - len(carry)
        piece = carry + digits[i:i + take]
        r = int(piece) % 97
        steps = steps + [[piece, r]]
        carry = str(r)
        i = i + take
    return [r, steps]

def rearranged(s):
    return s[4:len(s)] + s[0:4]

def check(s):
    lengths = countries()
    if len(s) < 5:
        return [False, "too short to be an IBAN"]
    if not (is_letter(s[0]) and is_letter(s[1])):
        return [False, "an IBAN starts with two letters, the country"]
    if not s[0:2] in lengths:
        return [False, s[0:2] + " is not a country in this list"]
    if not (is_digit(s[2]) and is_digit(s[3])):
        return [False, "the third and fourth characters, the check digits, must be digits"]
    if len(s) != lengths[s[0:2]]:
        return [False, s[0:2] + " IBANs have " + str(lengths[s[0:2]]) + " characters, this has " + str(len(s))]
    if mod97_steps(expand(rearranged(s)))[0] != 1:
        return [False, "mod 97 is not 1, so a character is wrong"]
    return [True, "mod 97 is 1"]

def check_digits(country, bban):
    r = mod97_steps(expand(bban + country + "00"))[0]
    d = 98 - r
    if d < 10:
        return "0" + str(d)
    return str(d)

def typos(s):
    digits = "0123456789"
    letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    out = []
    for i in range(0, len(s)):
        pool = digits
        if is_letter(s[i]):
            pool = letters
        for c in pool:
            if c != s[i]:
                t = s[0:i] + c + s[i + 1:len(s)]
                if check(t)[0]:
                    out = out + [["replace", t]]
    for i in range(0, len(s) - 2+1):
        if s[i] != s[i + 1]:
            t = s[0:i] + s[i + 1] + s[i] + s[i + 2:len(s)]
            if check(t)[0]:
                out = out + [["swap", t]]
    return out

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.