<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P028-spelling-suggester/ | updated: 2026-10-06 -->

# P028 Spelling suggester

Checks a line of text against a list of 352 common English words and suggests, for each word it does not know, the closest words by edit distance - insertions, deletions, substitutions and swaps of two neighbouring letters - the more common word first on a tie; suggests for a single word, and adds new words for the session.

EML-P project `projects/spelling-suggester` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/spelling-suggester` runs it and `eml project verify projects/spelling-suggester` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: edit-distance (https://efficientnewlanguage.org/cases/083-edit-distance/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
abc
6
1

1
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word x
123 + 456 = 579 !!!
2
two words
x1
a-b

2
it's
2
xyzzyq
4
3
the
3
'quote'
abcdefghijklmnopqrstu
Don't
3

1
zzzz qqq
1
'Don't' panic, it's fine.
4
5
```

Screen:

```text

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 0
Pick a number from 1 to 5.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> abc
Pick a number from 1 to 5.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 6
Pick a number from 1 to 5.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 1
text> 
Cancelled.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 1
text> word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word x
That line has 201 characters; at most 200. Type it again.
text> 123 + 456 = 579 !!!
No words in that line.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 2
word> two words
Type one word of letters (an apostrophe inside is fine), at most 20.
word> x1
Type one word of letters (an apostrophe inside is fine), at most 20.
word> a-b
Type one word of letters (an apostrophe inside is fine), at most 20.
word> 
Cancelled.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 2
word> it's
Closest to 'it's':
  its - 1 edit
  it - 2 edits
  is - 2 edits
  the - 3 edits
  to - 3 edits

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 2
word> xyzzyq
Nothing in the word list is within 3 edits of 'xyzzyq'.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 4
No words added yet.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 3
word> the
'the' is already in the word list.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 3
word> 'quote'
Type one word of letters (an apostrophe inside is fine), at most 20.
word> abcdefghijklmnopqrstu
Type one word of letters (an apostrophe inside is fine), at most 20.
word> Don't
Added 'don't'; the word list now has 353 words.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 3
word> 
Cancelled.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 1
text> zzzz qqq
2 words, 2 not in the word list:
  zzzz -> nothing within 2 edits
  qqq -> nothing within 2 edits

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 1
text> 'Don't' panic, it's fine.
4 words, 3 not in the word list:
  panic -> nothing within 2 edits
  it's -> its, it, is
  fine -> line, find, fire

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 4
Added this session: don't.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 5
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
Teh quikc brown fox jumpd over teh lazy dogg.
1
My freind found the langauge hard becuase of spelling.
2
wrld
2
The
3
receive
3
Tomorrow
2
recieve
1
I will receive the book tomorrow.
4
5
```

Screen:

```text

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 1
text> Teh quikc brown fox jumpd over teh lazy dogg.
9 words, 4 not in the word list:
  teh (2 times) -> the, be, to
  quikc -> quick
  jumpd -> jump, jumped
  dogg -> dog, do, does

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 1
text> My freind found the langauge hard becuase of spelling.
9 words, 3 not in the word list:
  freind -> friend, find
  langauge -> language
  becuase -> because

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 2
word> wrld
Closest to 'wrld':
  world - 1 edit
  will - 2 edits
  would - 2 edits
  well - 2 edits
  told - 2 edits

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 2
word> The
'the' is in the word list.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 3
word> receive
Added 'receive'; the word list now has 353 words.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 3
word> Tomorrow
Added 'tomorrow'; the word list now has 354 words.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 2
word> recieve
Closest to 'recieve':
  receive - 1 edit
  believe - 2 edits

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 1
text> I will receive the book tomorrow.
All 6 words are in the word list.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 4
Added this session: receive and tomorrow.

== Spelling suggester ==
1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P028 spelling suggester: check a line of text against a word list, suggest
# the closest words by edit distance, and add new words for this session.
import words
import spelling

200 => longest_line

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 listed(items):
    "" => out
    0 => i
    while i < len(items):
        if i > 0 and i == len(items) - 1:
            out + " and " => out
        elif i > 0:
            out + ", " => out
        out + items[i] => out
        i + 1 => i
    return out

def plural(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def one_word(s):
    # The word in lower case if s is one word of letters (an apostrophe inside
    # is fine), at most 20 characters; "" otherwise.
    trim(s) => s
    if s == "" or len(s) > 20:
        return ""
    spelling.words_of(s) => ws
    if len(ws) != 1 or len(ws[0]) != len(s):
        return ""
    return ws[0]

def ask_word():
    # A word, or "" when the answer is empty, which cancels.
    while True:
        input("word> ") => answer
        if trim(answer) == "":
            return ""
        one_word(answer) => w
        if w != "":
            return w
        "Type one word of letters (an apostrophe inside is fine), at most 20." ^0

def check(text, known, vocabulary):
    spelling.words_of(text) => ws
    if len(ws) == 0:
        "No words in that line." ^0
        return
    [] => unknown
    {} => times
    for w in ws:
        if not (w in known):
            if w in times:
                times[w] + 1 => times[w]
            else:
                1 => times[w]
                unknown + [w] => unknown
    if len(unknown) == 0:
        ("All " + plural(len(ws), "word") + " are in the word list.") ^0
        return
    (plural(len(ws), "word") + ", " + str(len(unknown)) + " not in the word list:") ^0
    for w in unknown:
        "  " + w => line
        if times[w] > 1:
            line + " (" + str(times[w]) + " times)" => line
        spelling.suggestions(w, vocabulary, 2, 3) => found
        if len(found) == 0:
            (line + " -> nothing within 2 edits") ^0
        else:
            "" => names
            for f in found:
                if names != "":
                    names + ", " => names
                names + f[0] => names
            (line + " -> " + names) ^0

def suggest(w, known, vocabulary):
    if w in known:
        ("'" + w + "' is in the word list.") ^0
        return
    spelling.suggestions(w, vocabulary, 3, 5) => found
    if len(found) == 0:
        ("Nothing in the word list is within 3 edits of '" + w + "'.") ^0
        return
    ("Closest to '" + w + "':") ^0
    for f in found:
        ("  " + f[0] + " - " + plural(f[1], "edit")) ^0

{} => known
[] => vocabulary
for w in words.common:
    True => known[w]
    vocabulary + [w] => vocabulary
[] => added

True => running
while running:
    "" ^0
    "== Spelling suggester ==" ^0
    "1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1":
        "" => text
        True => asking
        while asking:
            input("text> ") => text
            if len(text) > longest_line:
                ("That line has " + str(len(text)) + " characters; at most " + str(longest_line) + ". Type it again.") ^0
            else:
                False => asking
        if trim(text) == "":
            "Cancelled." ^0
        else:
            check(text, known, vocabulary)
    elif choice == "2":
        ask_word() => w
        if w == "":
            "Cancelled." ^0
        else:
            suggest(w, known, vocabulary)
    elif choice == "3":
        ask_word() => w
        if w == "":
            "Cancelled." ^0
        elif w in known:
            ("'" + w + "' is already in the word list.") ^0
        else:
            True => known[w]
            vocabulary + [w] => vocabulary
            added + [w] => added
            ("Added '" + w + "'; the word list now has " + str(len(vocabulary)) + " words.") ^0
    elif choice == "4":
        if len(added) == 0:
            "No words added yet." ^0
        else:
            ("Added this session: " + listed(added) + ".") ^0
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import words
import spelling
longest_line = 200

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 listed(items):
    out = ""
    i = 0
    while i < len(items):
        if i > 0 and i == len(items) - 1:
            out = out + " and "
        elif i > 0:
            out = out + ", "
        out = out + items[i]
        i = i + 1
    return out

def plural(n, word):
    if n == 1:
        return "1 " + word
    return str(n) + " " + word + "s"

def one_word(s):
    s = trim(s)
    if s == "" or len(s) > 20:
        return ""
    ws = spelling.words_of(s)
    if len(ws) != 1 or len(ws[0]) != len(s):
        return ""
    return ws[0]

def ask_word():
    while True:
        answer = input("word> ")
        if trim(answer) == "":
            return ""
        w = one_word(answer)
        if w != "":
            return w
        print("Type one word of letters (an apostrophe inside is fine), at most 20.")

def check(text, known, vocabulary):
    ws = spelling.words_of(text)
    if len(ws) == 0:
        print("No words in that line.")
        return
    unknown = []
    times = {}
    for w in ws:
        if not w in known:
            if w in times:
                times[w] = times[w] + 1
            else:
                times[w] = 1
                unknown = unknown + [w]
    if len(unknown) == 0:
        print("All " + plural(len(ws), "word") + " are in the word list.")
        return
    print(plural(len(ws), "word") + ", " + str(len(unknown)) + " not in the word list:")
    for w in unknown:
        line = "  " + w
        if times[w] > 1:
            line = line + " (" + str(times[w]) + " times)"
        found = spelling.suggestions(w, vocabulary, 2, 3)
        if len(found) == 0:
            print(line + " -> nothing within 2 edits")
        else:
            names = ""
            for f in found:
                if names != "":
                    names = names + ", "
                names = names + f[0]
            print(line + " -> " + names)

def suggest(w, known, vocabulary):
    if w in known:
        print("'" + w + "' is in the word list.")
        return
    found = spelling.suggestions(w, vocabulary, 3, 5)
    if len(found) == 0:
        print("Nothing in the word list is within 3 edits of '" + w + "'.")
        return
    print("Closest to '" + w + "':")
    for f in found:
        print("  " + f[0] + " - " + plural(f[1], "edit"))

known = {}
vocabulary = []
for w in words.common:
    known[w] = True
    vocabulary = vocabulary + [w]
added = []
running = True
while running:
    print("")
    print("== Spelling suggester ==")
    print("1) check a line  2) suggest for a word  3) add a word  4) added words  5) quit")
    choice = trim(input("choice> "))
    if choice == "1":
        text = ""
        asking = True
        while asking:
            text = input("text> ")
            if len(text) > longest_line:
                print("That line has " + str(len(text)) + " characters; at most " + str(longest_line) + ". Type it again.")
            else:
                asking = False
        if trim(text) == "":
            print("Cancelled.")
        else:
            check(text, known, vocabulary)
    elif choice == "2":
        w = ask_word()
        if w == "":
            print("Cancelled.")
        else:
            suggest(w, known, vocabulary)
    elif choice == "3":
        w = ask_word()
        if w == "":
            print("Cancelled.")
        elif w in known:
            print("'" + w + "' is already in the word list.")
        else:
            known[w] = True
            vocabulary = vocabulary + [w]
            added = added + [w]
            print("Added '" + w + "'; the word list now has " + str(len(vocabulary)) + " words.")
    elif choice == "4":
        if len(added) == 0:
            print("No words added yet.")
        else:
            print("Added this session: " + listed(added) + ".")
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### spelling.eml

```eml
# P028 spelling suggester - edit distance, the words of a text, and
# suggestions.

"abcdefghijklmnopqrstuvwxyz" => small
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" => capital

def distance(a, b, limit):
    # The fewest one-letter insertions, deletions and substitutions, and swaps
    # of two neighbouring letters, that turn a into b - the Levenshtein table
    # of the corpus case edit-distance with one more way into a cell (the
    # "optimal string alignment" distance). Anything over limit comes back as
    # limit + 1, at once when the lengths alone differ by more.
    len(a) => n
    len(b) => m
    if n - m > limit or m - n > limit:
        return limit + 1
    [] => t
    for i in [0:n]:
        [0] * (m + 1) => row
        i => row[0]
        t + [row] => t
    for j in [0:m]:
        j => t[0][j]
    for i in [1:n]:
        for j in [1:m]:
            1 => cost
            if a[i - 1] == b[j - 1]:
                0 => cost
            t[i - 1][j] + 1 => best
            if t[i][j - 1] + 1 < best:
                t[i][j - 1] + 1 => best
            if t[i - 1][j - 1] + cost < best:
                t[i - 1][j - 1] + cost => best
            if i > 1 and j > 1 and a[i - 1] == b[j - 2] and a[i - 2] == b[j - 1]:
                if t[i - 2][j - 2] + 1 < best:
                    t[i - 2][j - 2] + 1 => best
            best => t[i][j]
    if t[n][m] > limit:
        return limit + 1
    return t[n][m]

def lower_letter(c):
    # c in lower case if it is a letter, "'" for an apostrophe, else "".
    if c == "'":
        return c
    0 => k
    while k < 26:
        if small[k] == c or capital[k] == c:
            return small[k]
        k + 1 => k
    return ""

def without_apostrophes_around(w):
    0 => i
    len(w) => j
    while i < j and w[i] == "'":
        i + 1 => i
    while j > i and w[j - 1] == "'":
        j - 1 => j
    return w[i:j]

def words_of(text):
    # The words of a text in lower case: runs of letters and apostrophes, with
    # apostrophes at either end dropped, so "don't" stays one word.
    [] => out
    "" => w
    for c in text + " ":
        lower_letter(c) => l
        if l != "":
            w + l => w
        else:
            without_apostrophes_around(w) => s
            if s != "":
                out + [s] => out
            "" => w
    return out

def suggestions(word, vocabulary, limit, most):
    # Up to `most` words of the vocabulary within `limit` edits of word, as
    # [word, edits] pairs, nearest first; between equally near words the one
    # earlier in the vocabulary (the more common one) comes first.
    [] => found
    for w in vocabulary:
        distance(word, w, limit) => d
        if d <= limit:
            len(found) => j
            while j > 0 and found[j - 1][1] > d:
                j - 1 => j
            found[0:j] + [[w, d]] + found[j:len(found)] => found
            if len(found) > most:
                found[0:most] => found
    return found
```

Python projection of spelling.eml:

```python
small = "abcdefghijklmnopqrstuvwxyz"
capital = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

def distance(a, b, limit):
    n = len(a)
    m = len(b)
    if n - m > limit or m - n > limit:
        return limit + 1
    t = []
    for i in range(0, n+1):
        row = [0] * (m + 1)
        row[0] = i
        t = t + [row]
    for j in range(0, m+1):
        t[0][j] = j
    for i in range(1, n+1):
        for j in range(1, m+1):
            cost = 1
            if a[i - 1] == b[j - 1]:
                cost = 0
            best = t[i - 1][j] + 1
            if t[i][j - 1] + 1 < best:
                best = t[i][j - 1] + 1
            if t[i - 1][j - 1] + cost < best:
                best = t[i - 1][j - 1] + cost
            if i > 1 and j > 1 and a[i - 1] == b[j - 2] and a[i - 2] == b[j - 1]:
                if t[i - 2][j - 2] + 1 < best:
                    best = t[i - 2][j - 2] + 1
            t[i][j] = best
    if t[n][m] > limit:
        return limit + 1
    return t[n][m]

def lower_letter(c):
    if c == "'":
        return c
    k = 0
    while k < 26:
        if small[k] == c or capital[k] == c:
            return small[k]
        k = k + 1
    return ""

def without_apostrophes_around(w):
    i = 0
    j = len(w)
    while i < j and w[i] == "'":
        i = i + 1
    while j > i and w[j - 1] == "'":
        j = j - 1
    return w[i:j]

def words_of(text):
    out = []
    w = ""
    for c in text + " ":
        l = lower_letter(c)
        if l != "":
            w = w + l
        else:
            s = without_apostrophes_around(w)
            if s != "":
                out = out + [s]
            w = ""
    return out

def suggestions(word, vocabulary, limit, most):
    found = []
    for w in vocabulary:
        d = distance(word, w, limit)
        if d <= limit:
            j = len(found)
            while j > 0 and found[j - 1][1] > d:
                j = j - 1
            found = found[0:j] + [[w, d]] + found[j:len(found)]
            if len(found) > most:
                found = found[0:most]
    return found
```

### words.eml

```eml
# P028 spelling suggester - the built-in word list: 352 common English words,
# most common first. When two suggestions are the same number of edits away,
# the one that comes first here comes first.

[
    "the", "be", "to", "of", "and", "a", "in", "that", "have", "i", "it",
    "for", "not", "on", "with", "he", "as", "you", "do", "at", "this", "but",
    "his", "by", "from", "they", "we", "say", "her", "she", "or", "an",
    "will", "my", "one", "all", "would", "there", "their", "what", "so",
    "up", "out", "if", "about", "who", "get", "which", "go", "me", "when",
    "make", "can", "like", "time", "no", "just", "him", "know", "take",
    "people", "into", "year", "your", "good", "some", "could", "them", "see",
    "other", "than", "then", "now", "look", "only", "come", "its", "over",
    "think", "also", "back", "after", "use", "two", "how", "our", "work",
    "first", "well", "way", "even", "new", "want", "because", "any", "these",
    "give", "day", "most", "us", "is", "was", "are", "been", "has", "had",
    "were", "said", "did", "made", "went", "came", "saw", "got", "gave",
    "took", "found", "told", "thought", "called", "asked", "going", "being",
    "does", "done", "man", "men", "woman", "child", "children", "world",
    "life", "hand", "part", "place", "case", "week", "point", "number",
    "home", "water", "room", "mother", "father", "night", "thing", "things",
    "name", "house", "book", "word", "words", "story", "school", "family",
    "friend", "city", "country", "money", "eye", "eyes", "head", "face",
    "door", "line", "end", "side", "kind", "group", "problem", "question",
    "idea", "fact", "light", "song", "game", "paper", "picture", "letter",
    "language", "sentence", "spelling", "great", "little", "old", "big",
    "high", "different", "small", "large", "next", "early", "young",
    "important", "few", "public", "bad", "same", "able", "last", "long",
    "best", "better", "right", "left", "full", "fast", "quick", "quickly",
    "brown", "lazy", "black", "white", "red", "green", "blue", "cold", "hot",
    "hard", "easy", "real", "sure", "free", "find", "tell", "ask", "seem",
    "feel", "try", "leave", "call", "keep", "let", "begin", "help", "talk",
    "turn", "start", "show", "hear", "play", "run", "move", "live",
    "believe", "hold", "bring", "happen", "write", "sit", "stand", "lose",
    "pay", "meet", "include", "continue", "set", "learn", "change", "lead",
    "understand", "watch", "follow", "stop", "create", "speak", "read",
    "spend", "grow", "open", "walk", "win", "offer", "remember", "love",
    "consider", "appear", "buy", "wait", "serve", "die", "send", "expect",
    "build", "stay", "fall", "cut", "reach", "kill", "remain", "jump",
    "jumped", "spell", "draw", "carry", "eat", "here", "where", "why",
    "each", "between", "under", "never", "again", "always", "often", "still",
    "once", "soon", "together", "without", "until", "while", "through",
    "around", "below", "above", "against", "away", "down", "off", "too",
    "very", "really", "almost", "enough", "every", "something", "sometimes",
    "another", "such", "both", "own", "later", "dog", "fox", "cat", "bird",
    "tree", "river", "sea", "earth", "fire", "food", "car", "street",
    "state", "mile", "animal", "plant", "page", "list"
] => common
```

Python projection of words.eml:

```python
common = ["the", "be", "to", "of", "and", "a", "in", "that", "have", "i", "it", "for", "not", "on", "with", "he", "as", "you", "do", "at", "this", "but", "his", "by", "from", "they", "we", "say", "her", "she", "or", "an", "will", "my", "one", "all", "would", "there", "their", "what", "so", "up", "out", "if", "about", "who", "get", "which", "go", "me", "when", "make", "can", "like", "time", "no", "just", "him", "know", "take", "people", "into", "year", "your", "good", "some", "could", "them", "see", "other", "than", "then", "now", "look", "only", "come", "its", "over", "think", "also", "back", "after", "use", "two", "how", "our", "work", "first", "well", "way", "even", "new", "want", "because", "any", "these", "give", "day", "most", "us", "is", "was", "are", "been", "has", "had", "were", "said", "did", "made", "went", "came", "saw", "got", "gave", "took", "found", "told", "thought", "called", "asked", "going", "being", "does", "done", "man", "men", "woman", "child", "children", "world", "life", "hand", "part", "place", "case", "week", "point", "number", "home", "water", "room", "mother", "father", "night", "thing", "things", "name", "house", "book", "word", "words", "story", "school", "family", "friend", "city", "country", "money", "eye", "eyes", "head", "face", "door", "line", "end", "side", "kind", "group", "problem", "question", "idea", "fact", "light", "song", "game", "paper", "picture", "letter", "language", "sentence", "spelling", "great", "little", "old", "big", "high", "different", "small", "large", "next", "early", "young", "important", "few", "public", "bad", "same", "able", "last", "long", "best", "better", "right", "left", "full", "fast", "quick", "quickly", "brown", "lazy", "black", "white", "red", "green", "blue", "cold", "hot", "hard", "easy", "real", "sure", "free", "find", "tell", "ask", "seem", "feel", "try", "leave", "call", "keep", "let", "begin", "help", "talk", "turn", "start", "show", "hear", "play", "run", "move", "live", "believe", "hold", "bring", "happen", "write", "sit", "stand", "lose", "pay", "meet", "include", "continue", "set", "learn", "change", "lead", "understand", "watch", "follow", "stop", "create", "speak", "read", "spend", "grow", "open", "walk", "win", "offer", "remember", "love", "consider", "appear", "buy", "wait", "serve", "die", "send", "expect", "build", "stay", "fall", "cut", "reach", "kill", "remain", "jump", "jumped", "spell", "draw", "carry", "eat", "here", "where", "why", "each", "between", "under", "never", "again", "always", "often", "still", "once", "soon", "together", "without", "until", "while", "through", "around", "below", "above", "against", "away", "down", "off", "too", "very", "really", "almost", "enough", "every", "something", "sometimes", "another", "such", "both", "own", "later", "dog", "fox", "cat", "bird", "tree", "river", "sea", "earth", "fire", "food", "car", "street", "state", "mile", "animal", "plant", "page", "list"]
```

## README

# P028 - Spelling suggester

Checks a line of text against a list of 352 common English words. For each
word it does not know, it suggests the closest words in the list by edit
distance. It also suggests for a single word, and new words can be added for
the session.

- `main.eml` - the menu, the questions and their checks, and what the screen
  shows
- `spelling.eml` - the edit distance, the words of a text, and suggestions
- `words.eml` - the word list, most common words first

How each part works:

- The edit distance is the fewest one-letter insertions, deletions and
  substitutions, and swaps of two neighbouring letters, that turn one word
  into another. It is the Levenshtein table of the corpus case
  `edit-distance` with one more way into a cell, for a swap: `teh` is one
  edit from `the`, not two. (This "optimal string alignment" distance never
  edits a stretch twice, so `ca` is three edits from `abc`.)
- Only distances that matter are worked out in full: a word whose length
  differs by more than the limit is skipped at once.
- Suggestions are the words within 2 edits when checking a line (at most 3)
  and within 3 edits for a single word (at most 5), nearest first. Two words
  equally near go in the order of the list, which runs from the most common
  word down, so `teh` suggests `the` before `be` and `to`.
- A word is a run of letters and apostrophes in any case; apostrophes at
  either end are dropped, so `don't` and `it's` are single words and
  `'quoted'` is `quoted`. A word that appears several times is reported once,
  with how many times.
- Added words go to the end of the list, so they rank after the built-in
  words they tie with; they last until the program ends.

What is checked: a line is at most 200 characters; a single word is letters
with apostrophes only inside, at most 20 characters; a word already in the
list is not added again. An empty answer cancels.

Sessions: `sessions/basic.in` checks a line with `teh` twice, `quikc`,
`jumpd` and `dogg`, and a line with `freind`, `langauge` and `becuase`,
suggests for `wrld`, finds `The` in the list, adds `receive` and `Tomorrow`,
suggests for `recieve` (now `receive` first), checks a line that is all known
words, and lists the added words; `sessions/bad-input.in` types menu choices
that are not 1 to 5, an empty line, 201 characters, a line with no words,
two words, `x1` and `a-b` as one word, `it's`, a word with nothing near,
`the` to add again, `'quote'` and 21 letters, adds `Don't`, and checks a line
with nothing near and a line with apostrophes.

Built on the verified corpus case `edit-distance` (the Levenshtein distance
as a bottom-up table).
