{
  "id": "P016",
  "slug": "text-statistics",
  "key": "P016-text-statistics",
  "title": "Text statistics",
  "summary": "Type several lines of text and see its counts (lines, words, letters, average and longest word), its most common words, a chart of its letters, the palindromes in it - words and whole lines - and the groups of words that are anagrams of each other, from a text menu.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P016 - Text statistics\n\nType several lines of text - a line with just a dot ends it - and look at it\nfive ways: its counts (lines, words, letters, characters, the average and the\nlongest word), its most common words, a chart of its letters, the palindromes\nin it, and the groups of words in it that are anagrams of each other. New\ntext replaces the old; up to 200 lines are kept.\n\n- `main.eml` - the menu, reading the text, and what the screen shows\n- `analyze.eml` - splitting lines into words, counting words and letters,\n  sorting by count, and finding palindromes and anagrams\n- `text.eml` - trimming, lower case, letters and digits, reversing, sorting\n  the letters of a word, and rounding\n\nA word is a run of letters, digits and apostrophes, with apostrophes at\neither end taken off - \"don't\" is one word, 'tis is \"tis\" - and words are\ncompared in lower case. The common words are sorted by count, largest first,\nand words with the same count in alphabetical order, so the list is the same\nevery time. Each letter of the chart has its count, its share of all letters\nto one decimal (rounded half up in whole numbers) and a bar scaled so the\nmost common letter is 40 wide.\n\nA palindrome word has at least three letters and reads the same backwards\n(bob, kayak, noon, level); a palindrome line is one whose letters, ignoring\ncase, spaces and punctuation, read the same backwards (\"Was it a car or a cat\nI saw?\"). An anagram group is two or more different words of at least three\nletters made of exactly the same letters (listen, silent, enlist): sorting the\nletters of each word gives the same key. The interpreter that checks every\nsession does not run string or list methods yet, so reversing, sorting and\njoining are written out by hand.\n\nWhat is checked: a menu choice is one of 1 to 7, and anything but entering\ntext needs some text first; an empty text (just the dot) leaves the old text\nas it was.\n\nSessions: `sessions/basic.in` types five lines with three anagram groups,\nfour palindrome words and two palindrome lines, then shows every view;\n`sessions/bad-input.in` asks for views before any text, picks numbers that\nare not on the menu, ends a text before it starts, then types numbers,\na line of dots, apostrophes and quotes, and an empty line - a text with no\npalindromes and no anagrams.\n\nBuilt on the verified corpus cases `word-frequency-counter` (counting words\nin order of first appearance), `char-frequency-table` (counting characters,\nwith a bar for each), `palindrome-checker` (a word against its reverse) and\n`anagram-checker` (two words made of the same letters).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P016 text statistics: type several lines of text, then look at its counts,\n# its most common words, a chart of its letters, its palindromes and its\n# anagrams. New text replaces the old.\nimport analyze\nimport text\n\n200 => most_lines\n10 => most_common\n40 => widest_bar\n\ndef plural(n, word):\n    if n == 1:\n        return \"1 \" + word\n    return str(n) + \" \" + word + \"s\"\n\ndef row(label, value):\n    return \"  \" + (\"%-16s\" % label) + (\"%6s\" % value)\n\ndef joined(items):\n    \"\" => out\n    for i in [0:len(items) - 1]:\n        if i > 0:\n            out + \", \" => out\n        out + items[i] => out\n    return out\n\ndef read_text():\n    # Lines typed until a line that is just a dot; at most most_lines kept.\n    \"Type the text, one line at a time; a line with just . ends it.\" ^0\n    [] => lines\n    False => full\n    while True:\n        input(\"> \") => line\n        if text.trim(line) == \".\":\n            return lines\n        if len(lines) < most_lines:\n            lines + [line] => lines\n        elif not full:\n            (\"At most \" + str(most_lines) + \" lines; the rest is left out.\") ^0\n            True => full\n\ndef show_counts(lines):\n    analyze.all_words(lines) => ws\n    0 => with_text\n    0 => chars\n    0 => letters\n    for line in lines:\n        chars + len(line) => chars\n        letters + len(analyze.letters_only(line)) => letters\n        if text.trim(line) != \"\":\n            with_text + 1 => with_text\n    0 => word_chars\n    \"\" => longest\n    for w in ws:\n        word_chars + len(w) => word_chars\n        if len(w) > len(longest):\n            w => longest\n    \"\" ^0\n    \"-- counts --\" ^0\n    (row(\"lines\", str(len(lines))) + \"   (\" + str(with_text) + \" with text)\") ^0\n    row(\"words\", str(len(ws))) ^0\n    row(\"letters\", str(letters)) ^0\n    (row(\"characters\", str(chars)) + \"   (with spaces and punctuation)\") ^0\n    if len(ws) == 0:\n        row(\"average word\", \"-\") ^0\n        row(\"longest word\", \"-\") ^0\n    else:\n        (row(\"average word\", text.tenths(word_chars, len(ws))) + \"   characters\") ^0\n        (\"  \" + (\"%-16s\" % \"longest word\") + longest + \" (\" + str(len(longest)) + \" characters)\") ^0\n\ndef show_common(lines):\n    analyze.counted(analyze.all_words(lines)) => pairs\n    \"\" ^0\n    \"-- common words --\" ^0\n    if len(pairs) == 0:\n        \"  (no words)\" ^0\n    else:\n        0 => total\n        for p in pairs:\n            total + p[1] => total\n        (\"  \" + str(total) + \" words, \" + str(len(pairs)) + \" different; the most common:\") ^0\n        0 => shown\n        for p in analyze.by_count(pairs):\n            if shown < most_common:\n                ((\"%6d\" % p[1]) + \"  \" + p[0]) ^0\n                shown + 1 => shown\n\ndef show_letters(lines):\n    analyze.letter_counts(lines) => counts\n    0 => total\n    0 => most\n    for p in counts:\n        total + p[1] => total\n        if p[1] > most:\n            p[1] => most\n    \"\" ^0\n    if total == 0:\n        \"-- letters --\" ^0\n        \"  (no letters)\" ^0\n    else:\n        (\"-- letters (\" + str(total) + \" in all) --\") ^0\n        for p in counts:\n            (\"  \" + p[0] + (\"%5d\" % p[1]) + (\"%7s\" % (text.tenths(100 * p[1], total) + \"%\")) + \"  \" + (\"#\" * text.half_up(widest_bar * p[1], most))) ^0\n\ndef show_palindromes(lines):\n    analyze.palindrome_words(analyze.all_words(lines)) => ws\n    analyze.palindrome_lines(lines) => ls\n    \"\" ^0\n    \"-- palindromes --\" ^0\n    if len(ws) == 0:\n        \"  words: none\" ^0\n    else:\n        (\"  words: \" + joined(ws)) ^0\n    if len(ls) == 0:\n        \"  lines: none\" ^0\n    else:\n        \"  lines:\" ^0\n        for line in ls:\n            (\"    \" + line) ^0\n\ndef show_anagrams(lines):\n    analyze.anagram_groups(analyze.all_words(lines)) => groups\n    \"\" ^0\n    \"-- anagrams --\" ^0\n    if len(groups) == 0:\n        \"  none\" ^0\n    for g in groups:\n        (\"  \" + joined(g)) ^0\n\n[] => lines\nTrue => running\nwhile running:\n    \"\" ^0\n    \"== Text statistics ==\" ^0\n    \"1) enter text  2) counts  3) common words  4) letter chart\" ^0\n    \"5) palindromes  6) anagrams  7) quit\" ^0\n    text.trim(input(\"choice> \")) => choice\n    if choice == \"1\":\n        read_text() => typed\n        if len(typed) == 0:\n            \"No text entered; the text is as it was.\" ^0\n        else:\n            typed => lines\n            (\"Read \" + plural(len(lines), \"line\") + \", \" + plural(len(analyze.all_words(lines)), \"word\") + \".\") ^0\n    elif choice == \"7\":\n        False => running\n    elif len(choice) == 1 and choice in \"23456\" and len(lines) == 0:\n        \"Enter some text first.\" ^0\n    elif choice == \"2\":\n        show_counts(lines)\n    elif choice == \"3\":\n        show_common(lines)\n    elif choice == \"4\":\n        show_letters(lines)\n    elif choice == \"5\":\n        show_palindromes(lines)\n    elif choice == \"6\":\n        show_anagrams(lines)\n    else:\n        \"Pick a number from 1 to 7.\" ^0\n\"Bye.\" ^0\n",
      "python": "import analyze\nimport text\nmost_lines = 200\nmost_common = 10\nwidest_bar = 40\n\ndef plural(n, word):\n    if n == 1:\n        return \"1 \" + word\n    return str(n) + \" \" + word + \"s\"\n\ndef row(label, value):\n    return \"  \" + \"%-16s\" % label + \"%6s\" % value\n\ndef joined(items):\n    out = \"\"\n    for i in range(0, len(items)):\n        if i > 0:\n            out = out + \", \"\n        out = out + items[i]\n    return out\n\ndef read_text():\n    print(\"Type the text, one line at a time; a line with just . ends it.\")\n    lines = []\n    full = False\n    while True:\n        line = input(\"> \")\n        if text.trim(line) == \".\":\n            return lines\n        if len(lines) < most_lines:\n            lines = lines + [line]\n        elif not full:\n            print(\"At most \" + str(most_lines) + \" lines; the rest is left out.\")\n            full = True\n\ndef show_counts(lines):\n    ws = analyze.all_words(lines)\n    with_text = 0\n    chars = 0\n    letters = 0\n    for line in lines:\n        chars = chars + len(line)\n        letters = letters + len(analyze.letters_only(line))\n        if text.trim(line) != \"\":\n            with_text = with_text + 1\n    word_chars = 0\n    longest = \"\"\n    for w in ws:\n        word_chars = word_chars + len(w)\n        if len(w) > len(longest):\n            longest = w\n    print(\"\")\n    print(\"-- counts --\")\n    print(row(\"lines\", str(len(lines))) + \"   (\" + str(with_text) + \" with text)\")\n    print(row(\"words\", str(len(ws))))\n    print(row(\"letters\", str(letters)))\n    print(row(\"characters\", str(chars)) + \"   (with spaces and punctuation)\")\n    if len(ws) == 0:\n        print(row(\"average word\", \"-\"))\n        print(row(\"longest word\", \"-\"))\n    else:\n        print(row(\"average word\", text.tenths(word_chars, len(ws))) + \"   characters\")\n        print(\"  \" + \"%-16s\" % \"longest word\" + longest + \" (\" + str(len(longest)) + \" characters)\")\n\ndef show_common(lines):\n    pairs = analyze.counted(analyze.all_words(lines))\n    print(\"\")\n    print(\"-- common words --\")\n    if len(pairs) == 0:\n        print(\"  (no words)\")\n    else:\n        total = 0\n        for p in pairs:\n            total = total + p[1]\n        print(\"  \" + str(total) + \" words, \" + str(len(pairs)) + \" different; the most common:\")\n        shown = 0\n        for p in analyze.by_count(pairs):\n            if shown < most_common:\n                print(\"%6d\" % p[1] + \"  \" + p[0])\n                shown = shown + 1\n\ndef show_letters(lines):\n    counts = analyze.letter_counts(lines)\n    total = 0\n    most = 0\n    for p in counts:\n        total = total + p[1]\n        if p[1] > most:\n            most = p[1]\n    print(\"\")\n    if total == 0:\n        print(\"-- letters --\")\n        print(\"  (no letters)\")\n    else:\n        print(\"-- letters (\" + str(total) + \" in all) --\")\n        for p in counts:\n            print(\"  \" + p[0] + \"%5d\" % p[1] + \"%7s\" % (text.tenths(100 * p[1], total) + \"%\") + \"  \" + \"#\" * text.half_up(widest_bar * p[1], most))\n\ndef show_palindromes(lines):\n    ws = analyze.palindrome_words(analyze.all_words(lines))\n    ls = analyze.palindrome_lines(lines)\n    print(\"\")\n    print(\"-- palindromes --\")\n    if len(ws) == 0:\n        print(\"  words: none\")\n    else:\n        print(\"  words: \" + joined(ws))\n    if len(ls) == 0:\n        print(\"  lines: none\")\n    else:\n        print(\"  lines:\")\n        for line in ls:\n            print(\"    \" + line)\n\ndef show_anagrams(lines):\n    groups = analyze.anagram_groups(analyze.all_words(lines))\n    print(\"\")\n    print(\"-- anagrams --\")\n    if len(groups) == 0:\n        print(\"  none\")\n    for g in groups:\n        print(\"  \" + joined(g))\n\nlines = []\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"== Text statistics ==\")\n    print(\"1) enter text  2) counts  3) common words  4) letter chart\")\n    print(\"5) palindromes  6) anagrams  7) quit\")\n    choice = text.trim(input(\"choice> \"))\n    if choice == \"1\":\n        typed = read_text()\n        if len(typed) == 0:\n            print(\"No text entered; the text is as it was.\")\n        else:\n            lines = typed\n            print(\"Read \" + plural(len(lines), \"line\") + \", \" + plural(len(analyze.all_words(lines)), \"word\") + \".\")\n    elif choice == \"7\":\n        running = False\n    elif len(choice) == 1 and choice in \"23456\" and len(lines) == 0:\n        print(\"Enter some text first.\")\n    elif choice == \"2\":\n        show_counts(lines)\n    elif choice == \"3\":\n        show_common(lines)\n    elif choice == \"4\":\n        show_letters(lines)\n    elif choice == \"5\":\n        show_palindromes(lines)\n    elif choice == \"6\":\n        show_anagrams(lines)\n    else:\n        print(\"Pick a number from 1 to 7.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "analyze.eml",
      "eml": "# P016 text statistics - what is counted and found in the text, which is a\n# list of lines. A word is a run of letters, digits and apostrophes, with\n# apostrophes at either end taken off (\"don't\" is one word, \"'tis'\" is\n# \"tis\"), and words are compared in lower case.\nimport text\n\ndef words_of(line):\n    [] => out\n    \"\" => word\n    for c in line + \" \":\n        if text.is_letter(c) or text.is_digit(c) or c == \"'\":\n            word + c => word\n        else:\n            while len(word) > 0 and word[0] == \"'\":\n                word[1:len(word)] => word\n            while len(word) > 0 and word[len(word) - 1] == \"'\":\n                word[0:len(word) - 1] => word\n            if word != \"\":\n                out + [text.lower(word)] => out\n            \"\" => word\n    return out\n\ndef all_words(lines):\n    [] => out\n    for line in lines:\n        out + words_of(line) => out\n    return out\n\ndef letters_only(s):\n    # The letters of s in lower case, everything else left out.\n    \"\" => out\n    for c in s:\n        if text.is_letter(c):\n            out + text.lower_char(c) => out\n    return out\n\ndef counted(words):\n    # [word, count] for every distinct word, in order of first appearance.\n    {} => seen\n    [] => out\n    for w in words:\n        if w in seen:\n            out[seen[w]][1] + 1 => out[seen[w]][1]\n        else:\n            len(out) => seen[w]\n            out + [[w, 1]] => out\n    return out\n\ndef by_count(pairs):\n    # pairs sorted by count, largest first, equal counts in alphabetical order\n    # (insertion sort, so the order of ties is fixed).\n    [] => out\n    for p in pairs:\n        len(out) => i\n        out + [p] => out\n        while i > 0 and (out[i - 1][1] < p[1] or (out[i - 1][1] == p[1] and out[i - 1][0] > p[0])):\n            out[i - 1] => out[i]\n            i - 1 => i\n        p => out[i]\n    return out\n\ndef letter_counts(lines):\n    # [letter, count] for each of a-z that occurs, in alphabetical order.\n    [] => counts\n    for i in [0:25]:\n        counts + [[text.lower_letters[i], 0]] => counts\n    for line in lines:\n        for c in letters_only(line):\n            for i in [0:25]:\n                if text.lower_letters[i] == c:\n                    counts[i][1] + 1 => counts[i][1]\n    [] => out\n    for p in counts:\n        if p[1] > 0:\n            out + [p] => out\n    return out\n\ndef is_palindrome(s):\n    return s == text.reverse(s)\n\ndef palindrome_words(words):\n    # Distinct words of three or more letters that read the same backwards.\n    [] => out\n    for p in counted(words):\n        if len(p[0]) >= 3 and letters_only(p[0]) == p[0] and is_palindrome(p[0]):\n            out + [p[0]] => out\n    return out\n\ndef palindrome_lines(lines):\n    # Lines whose letters, ignoring case and everything else, read the same\n    # backwards - at least three letters of them.\n    [] => out\n    for line in lines:\n        letters_only(line) => s\n        if len(s) >= 3 and is_palindrome(s):\n            out + [text.trim(line)] => out\n    return out\n\ndef anagram_groups(words):\n    # Groups of two or more distinct words of three or more letters made of\n    # the same letters, in order of first appearance.\n    {} => where\n    [] => groups\n    for p in counted(words):\n        p[0] => w\n        if len(w) >= 3 and letters_only(w) == w:\n            text.sorted_chars(w) => key\n            if key in where:\n                groups[where[key]] + [w] => groups[where[key]]\n            else:\n                len(groups) => where[key]\n                groups + [[w]] => groups\n    [] => out\n    for g in groups:\n        if len(g) >= 2:\n            out + [g] => out\n    return out\n",
      "python": "import text\n\ndef words_of(line):\n    out = []\n    word = \"\"\n    for c in line + \" \":\n        if text.is_letter(c) or text.is_digit(c) or c == \"'\":\n            word = word + c\n        else:\n            while len(word) > 0 and word[0] == \"'\":\n                word = word[1:len(word)]\n            while len(word) > 0 and word[len(word) - 1] == \"'\":\n                word = word[0:len(word) - 1]\n            if word != \"\":\n                out = out + [text.lower(word)]\n            word = \"\"\n    return out\n\ndef all_words(lines):\n    out = []\n    for line in lines:\n        out = out + words_of(line)\n    return out\n\ndef letters_only(s):\n    out = \"\"\n    for c in s:\n        if text.is_letter(c):\n            out = out + text.lower_char(c)\n    return out\n\ndef counted(words):\n    seen = {}\n    out = []\n    for w in words:\n        if w in seen:\n            out[seen[w]][1] = out[seen[w]][1] + 1\n        else:\n            seen[w] = len(out)\n            out = out + [[w, 1]]\n    return out\n\ndef by_count(pairs):\n    out = []\n    for p in pairs:\n        i = len(out)\n        out = out + [p]\n        while i > 0 and (out[i - 1][1] < p[1] or out[i - 1][1] == p[1] and out[i - 1][0] > p[0]):\n            out[i] = out[i - 1]\n            i = i - 1\n        out[i] = p\n    return out\n\ndef letter_counts(lines):\n    counts = []\n    for i in range(0, 26):\n        counts = counts + [[text.lower_letters[i], 0]]\n    for line in lines:\n        for c in letters_only(line):\n            for i in range(0, 26):\n                if text.lower_letters[i] == c:\n                    counts[i][1] = counts[i][1] + 1\n    out = []\n    for p in counts:\n        if p[1] > 0:\n            out = out + [p]\n    return out\n\ndef is_palindrome(s):\n    return s == text.reverse(s)\n\ndef palindrome_words(words):\n    out = []\n    for p in counted(words):\n        if len(p[0]) >= 3 and letters_only(p[0]) == p[0] and is_palindrome(p[0]):\n            out = out + [p[0]]\n    return out\n\ndef palindrome_lines(lines):\n    out = []\n    for line in lines:\n        s = letters_only(line)\n        if len(s) >= 3 and is_palindrome(s):\n            out = out + [text.trim(line)]\n    return out\n\ndef anagram_groups(words):\n    where = {}\n    groups = []\n    for p in counted(words):\n        w = p[0]\n        if len(w) >= 3 and letters_only(w) == w:\n            key = text.sorted_chars(w)\n            if key in where:\n                groups[where[key]] = groups[where[key]] + [w]\n            else:\n                where[key] = len(groups)\n                groups = groups + [[w]]\n    out = []\n    for g in groups:\n        if len(g) >= 2:\n            out = out + [g]\n    return out\n"
    },
    {
      "name": "text.eml",
      "eml": "# P016 text statistics - characters and strings. The interpreter that checks\n# every session does not run string or list methods yet, so lower case,\n# reversing, sorting and joining are written out here.\n\n\"abcdefghijklmnopqrstuvwxyz\" => lower_letters\n\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\" => upper_letters\n\ndef trim(s):\n    # s without the spaces at either end.\n    0 => i\n    len(s) => j\n    while i < j and s[i] == \" \":\n        i + 1 => i\n    while j > i and s[j - 1] == \" \":\n        j - 1 => j\n    return s[i:j]\n\ndef lower_char(c):\n    for i in [0:25]:\n        if upper_letters[i] == c:\n            return lower_letters[i]\n    return c\n\ndef lower(s):\n    \"\" => out\n    for c in s:\n        out + lower_char(c) => out\n    return out\n\ndef is_letter(c):\n    return c in lower_letters or c in upper_letters\n\ndef is_digit(c):\n    return c in \"0123456789\"\n\ndef reverse(s):\n    \"\" => out\n    for c in s:\n        c + out => out\n    return out\n\ndef sorted_chars(s):\n    # The characters of s in order, as a string (insertion sort).\n    [] => cs\n    for c in s:\n        len(cs) => i\n        cs + [c] => cs\n        while i > 0 and cs[i - 1] > c:\n            cs[i - 1] => cs[i]\n            i - 1 => i\n        c => cs[i]\n    \"\" => out\n    for c in cs:\n        out + c => out\n    return out\n\ndef half_up(n, d):\n    # n / d rounded half up to a whole number, for n >= 0 and d > 0.\n    2 * n + d => t\n    return int((t - t % (2 * d)) / (2 * d))\n\ndef tenths(n, d):\n    # n / d to one decimal, rounded half up, as text: 19 / 5 is \"3.8\".\n    half_up(10 * n, d) => t\n    return str(int((t - t % 10) / 10)) + \".\" + str(t % 10)\n",
      "python": "lower_letters = \"abcdefghijklmnopqrstuvwxyz\"\nupper_letters = \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n\ndef trim(s):\n    i = 0\n    j = len(s)\n    while i < j and s[i] == \" \":\n        i = i + 1\n    while j > i and s[j - 1] == \" \":\n        j = j - 1\n    return s[i:j]\n\ndef lower_char(c):\n    for i in range(0, 26):\n        if upper_letters[i] == c:\n            return lower_letters[i]\n    return c\n\ndef lower(s):\n    out = \"\"\n    for c in s:\n        out = out + lower_char(c)\n    return out\n\ndef is_letter(c):\n    return c in lower_letters or c in upper_letters\n\ndef is_digit(c):\n    return c in \"0123456789\"\n\ndef reverse(s):\n    out = \"\"\n    for c in s:\n        out = c + out\n    return out\n\ndef sorted_chars(s):\n    cs = []\n    for c in s:\n        i = len(cs)\n        cs = cs + [c]\n        while i > 0 and cs[i - 1] > c:\n            cs[i] = cs[i - 1]\n            i = i - 1\n        cs[i] = c\n    out = \"\"\n    for c in cs:\n        out = out + c\n    return out\n\ndef half_up(n, d):\n    t = 2 * n + d\n    return int((t - t % (2 * d)) / (2 * d))\n\ndef tenths(n, d):\n    t = half_up(10 * n, d)\n    return str(int((t - t % 10) / 10)) + \".\" + str(t % 10)\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "2\n6\n8\n23\n1\n.\n1\n  123 456\n...\n'tis the \"end\" - don't stop\n\n.\n2\n3\n4\n5\n6\n7\n",
      "screen": "\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 2\nEnter some text first.\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 6\nEnter some text first.\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 8\nPick a number from 1 to 7.\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 23\nPick a number from 1 to 7.\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 1\nType the text, one line at a time; a line with just . ends it.\n> .\nNo text entered; the text is as it was.\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 1\nType the text, one line at a time; a line with just . ends it.\n>   123 456\n> ...\n> 'tis the \"end\" - don't stop\n> \n> .\nRead 4 lines, 7 words.\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 2\n\n-- counts --\n  lines                4   (3 with text)\n  words                7\n  letters             17\n  characters          39   (with spaces and punctuation)\n  average word       3.4   characters\n  longest word    don't (5 characters)\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 3\n\n-- common words --\n  7 words, 7 different; the most common:\n     1  123\n     1  456\n     1  don't\n     1  end\n     1  stop\n     1  the\n     1  tis\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 4\n\n-- letters (17 in all) --\n  d    2  11.8%  ####################\n  e    2  11.8%  ####################\n  h    1   5.9%  ##########\n  i    1   5.9%  ##########\n  n    2  11.8%  ####################\n  o    2  11.8%  ####################\n  p    1   5.9%  ##########\n  s    2  11.8%  ####################\n  t    4  23.5%  ########################################\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 5\n\n-- palindromes --\n  words: none\n  lines: none\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 6\n\n-- anagrams --\n  none\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\nListen to the silent night; enlist the stars.\nWas it a car or a cat I saw?\nEvil and vile are the same letters as live.\nBob saw a kayak at noon, and the level of the river rose.\nNever odd or even.\n.\n2\n3\n4\n5\n6\n7\n",
      "screen": "\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 1\nType the text, one line at a time; a line with just . ends it.\n> Listen to the silent night; enlist the stars.\n> Was it a car or a cat I saw?\n> Evil and vile are the same letters as live.\n> Bob saw a kayak at noon, and the level of the river rose.\n> Never odd or even.\n> .\nRead 5 lines, 43 words.\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 2\n\n-- counts --\n  lines                5   (5 with text)\n  words               43\n  letters            146\n  characters         191   (with spaces and punctuation)\n  average word       3.4   characters\n  longest word    letters (7 characters)\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 3\n\n-- common words --\n  43 words, 34 different; the most common:\n     5  the\n     3  a\n     2  and\n     2  or\n     2  saw\n     1  are\n     1  as\n     1  at\n     1  bob\n     1  car\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 4\n\n-- letters (146 in all) --\n  a   17  11.6%  ##############################\n  b    2   1.4%  ###\n  c    2   1.4%  ###\n  d    4   2.7%  #######\n  e   23  15.8%  ########################################\n  f    1   0.7%  ##\n  g    1   0.7%  ##\n  h    6   4.1%  ##########\n  i   10   6.8%  #################\n  k    2   1.4%  ###\n  l    9   6.2%  ################\n  m    1   0.7%  ##\n  n   10   6.8%  #################\n  o    9   6.2%  ################\n  r   10   6.8%  #################\n  s   12   8.2%  #####################\n  t   16  11.0%  ############################\n  v    7   4.8%  ############\n  w    3   2.1%  #####\n  y    1   0.7%  ##\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 5\n\n-- palindromes --\n  words: bob, kayak, noon, level\n  lines:\n    Was it a car or a cat I saw?\n    Never odd or even.\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 6\n\n-- anagrams --\n  listen, silent, enlist\n  was, saw\n  evil, vile, live\n\n== Text statistics ==\n1) enter text  2) counts  3) common words  4) letter chart\n5) palindromes  6) anagrams  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "word-frequency-counter",
      "caseId": "010-word-frequency-counter",
      "title": "Case corpus: a self-authored word-frequency counter"
    },
    {
      "slug": "char-frequency-table",
      "caseId": "157-char-frequency-table",
      "title": "Counting with a dict, in first-appearance order"
    },
    {
      "slug": "palindrome-checker",
      "caseId": "031-palindrome-checker",
      "title": "Palindrome checker"
    },
    {
      "slug": "anagram-checker",
      "caseId": "011-anagram-checker",
      "title": "Anagram checker"
    }
  ],
  "updated": "2026-10-03"
}
