{
  "id": "P090",
  "slug": "text-search",
  "key": "P090-text-search",
  "title": "Text search",
  "summary": "Every place a pattern occurs in a text, found by the naive method, Knuth-Morris-Pratt and Boyer-Moore-Horspool, with the character comparisons each makes; KMP's failure table for a pattern; and the longest common prefix of a list of words.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P090 - Text search\n\nEvery place a pattern occurs in a text, found three ways - the naive\nmethod, Knuth-Morris-Pratt and Boyer-Moore-Horspool - each counting the\ncharacter comparisons it makes. Also KMP's table for a pattern, and the\nlongest common start of a list of words. It starts with \"she sells sea\nshells by the sea shore; ...\".\n\n- `main.eml` - the menu, the text with its matches marked, and the reports\n- `finder.eml` - the three searches, KMP's failure table, and the common\n  prefix\n\nHow each part works:\n\n- Naive, as in the corpus case `substring-search`: try every starting\n  place, comparing left to right until a character differs.\n- Knuth-Morris-Pratt: the table says, for each start of the pattern, how\n  long its longest proper start is that is also its end - for abacabab,\n  0 0 1 0 1 2 3 2. On a mismatch the pattern slides on by that much\n  instead of going back in the text. Each comparison either moves on in\n  the text or slides the pattern on, and neither can happen more than n\n  times, so it makes at most 2n comparisons for a text of n.\n- Boyer-Moore-Horspool: compare from the pattern's last character\n  backwards. After each try, slide by how far the text character under the\n  pattern's end lies from the end of the pattern - or by the pattern's\n  whole length if it does not occur in it. On ordinary text it skips most\n  characters: 36 comparisons for \"shells\", against the naive 111.\n- Matches may overlap; all three report every one.\n- Longest common start, as in the corpus case `longest-common-prefix`: go\n  column by column until one word ends or two differ.\n\nWhat is checked: menu choices 1 to 5; a pattern of 1 to 20 characters; 2\nto 10 words; a text of 1 to 120 characters. An empty answer cancels.\n\nSessions: `sessions/basic.in` gives:\n- \"she\" (5 places), \"shells\" (3), \"sea shore\" (1) and \"xyz\" (none) in the\n  starting text, with the comparisons of each method;\n- KMP's tables for abacabab and aaaa;\n- the common start of flower, flow and flight (\"fl\"), and of dog and cat\n  (none);\n- a text of 29 a's and a b, searched for aaab: naive 108 comparisons,\n  KMP 56, Horspool 30.\n\n`sessions/bad-input.in` gives:\n- menu choices 0 and x;\n- a 21-character pattern, then empty patterns;\n- one word, and eleven;\n- a 121-character text, then a cancel.\n\nBuilt on the verified corpus cases `substring-search` (trying every\nstarting place) and `longest-common-prefix` (column by column).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P090 text search: every place a pattern occurs, found by the naive\n# method, Knuth-Morris-Pratt and Boyer-Moore-Horspool, with the comparisons\n# each makes; KMP's table for a pattern; and the longest common prefix.\nimport finder\n\ndef plain(s):\n    # Spaces at the ends removed (input keeps them otherwise).\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 joined(xs):\n    \"\" => s\n    for x in xs:\n        if s != \"\":\n            s + \" \" => s\n        s + str(x) => s\n    return s\n\ndef marks(n, places):\n    [\" \"] * n => row\n    for p in places:\n        \"^\" => row[p]\n    \"\" => s\n    for c in row:\n        s + c => s\n    len(s) => j\n    while j > 0 and s[j - 1] == \" \":\n        j - 1 => j\n    return s[0:j]\n\ndef search(text):\n    plain(input(\"pattern (1 to 20 characters)> \")) => pat\n    if pat == \"\":\n        \"Cancelled.\" ^0\n        return\n    if len(pat) > 20:\n        \"A pattern is up to 20 characters.\" ^0\n        return\n    finder.naive(text, pat) => a\n    finder.kmp(text, pat) => b\n    finder.horspool(text, pat) => c\n    (\"  \" + text) ^0\n    if len(a[0]) > 0:\n        (\"  \" + marks(len(text), a[0])) ^0\n    if len(a[0]) == 0:\n        (\"'\" + pat + \"' does not occur.\") ^0\n    else:\n        \"times\" => t\n        if len(a[0]) == 1:\n            \"time\" => t\n        (\"'\" + pat + \"' occurs \" + str(len(a[0])) + \" \" + t + \", starting at character \" + joined(a[0]) + \" (counting from 0).\") ^0\n    if a[0] == b[0] and b[0] == c[0]:\n        \"All three methods find the same places.\" ^0\n    (\"Comparisons: naive \" + str(a[1]) + \", KMP \" + str(b[1]) + \" (never more than 2 x \" + str(len(text)) + \"), Horspool \" + str(c[1]) + \".\") ^0\n\ndef table():\n    plain(input(\"pattern (1 to 20 characters)> \")) => pat\n    if pat == \"\" or len(pat) > 20:\n        \"Cancelled.\" ^0\n        return\n    finder.failure(pat) => f\n    \"\" => top\n    \"\" => bot\n    for i in [0:len(pat) - 1]:\n        top + \" \" + pat[i] => top\n        bot + \" \" + str(f[i]) => bot\n    (\"  pattern\" + top) ^0\n    (\"  table  \" + bot) ^0\n    \"Each number is the longest proper start of the pattern so far that is also its end: after a mismatch there, the pattern can slide on and keep that many characters matched.\" ^0\n\ndef prefix():\n    plain(input(\"words, separated by spaces (2 to 10)> \")) => s\n    if s == \"\":\n        \"Cancelled.\" ^0\n        return\n    [] => ws\n    \"\" => w\n    for c in s + \" \":\n        if c == \" \":\n            if w != \"\":\n                ws + [w] => ws\n            \"\" => w\n        else:\n            w + c => w\n    if len(ws) < 2 or len(ws) > 10:\n        \"Type 2 to 10 words.\" ^0\n        return\n    finder.common_prefix(ws) => p\n    if p == \"\":\n        \"They have no common start.\" ^0\n    else:\n        (\"Longest common start: '\" + p + \"' (\" + str(len(p)) + \" characters).\") ^0\n\ndef new_text(text):\n    plain(input(\"text (1 to 120 characters)> \")) => s\n    if s == \"\":\n        \"Cancelled.\" ^0\n        return text\n    if len(s) > 120:\n        \"A text is up to 120 characters.\" ^0\n        return text\n    (\"A text of \" + str(len(s)) + \" characters.\") ^0\n    return s\n\n\"she sells sea shells by the sea shore; the shells she sells are sea shells for sure\" => text\n\n\"== Text search ==\" ^0\n\"Every place a pattern occurs: naive, Knuth-Morris-Pratt and Boyer-Moore-Horspool, comparisons counted.\" ^0\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) search  2) KMP table  3) common start of words  4) new text  5) quit\" ^0\n    plain(input(\"choice> \")) => choice\n    if choice == \"1\":\n        search(text)\n    elif choice == \"2\":\n        table()\n    elif choice == \"3\":\n        prefix()\n    elif choice == \"4\":\n        new_text(text) => text\n    elif choice == \"5\":\n        False => running\n    else:\n        \"Pick a number from 1 to 5.\" ^0\n\"Bye.\" ^0\n",
      "python": "import finder\n\ndef plain(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 joined(xs):\n    s = \"\"\n    for x in xs:\n        if s != \"\":\n            s = s + \" \"\n        s = s + str(x)\n    return s\n\ndef marks(n, places):\n    row = [\" \"] * n\n    for p in places:\n        row[p] = \"^\"\n    s = \"\"\n    for c in row:\n        s = s + c\n    j = len(s)\n    while j > 0 and s[j - 1] == \" \":\n        j = j - 1\n    return s[0:j]\n\ndef search(text):\n    pat = plain(input(\"pattern (1 to 20 characters)> \"))\n    if pat == \"\":\n        print(\"Cancelled.\")\n        return\n    if len(pat) > 20:\n        print(\"A pattern is up to 20 characters.\")\n        return\n    a = finder.naive(text, pat)\n    b = finder.kmp(text, pat)\n    c = finder.horspool(text, pat)\n    print(\"  \" + text)\n    if len(a[0]) > 0:\n        print(\"  \" + marks(len(text), a[0]))\n    if len(a[0]) == 0:\n        print(\"'\" + pat + \"' does not occur.\")\n    else:\n        t = \"times\"\n        if len(a[0]) == 1:\n            t = \"time\"\n        print(\"'\" + pat + \"' occurs \" + str(len(a[0])) + \" \" + t + \", starting at character \" + joined(a[0]) + \" (counting from 0).\")\n    if a[0] == b[0] and b[0] == c[0]:\n        print(\"All three methods find the same places.\")\n    print(\"Comparisons: naive \" + str(a[1]) + \", KMP \" + str(b[1]) + \" (never more than 2 x \" + str(len(text)) + \"), Horspool \" + str(c[1]) + \".\")\n\ndef table():\n    pat = plain(input(\"pattern (1 to 20 characters)> \"))\n    if pat == \"\" or len(pat) > 20:\n        print(\"Cancelled.\")\n        return\n    f = finder.failure(pat)\n    top = \"\"\n    bot = \"\"\n    for i in range(0, len(pat)):\n        top = top + \" \" + pat[i]\n        bot = bot + \" \" + str(f[i])\n    print(\"  pattern\" + top)\n    print(\"  table  \" + bot)\n    print(\"Each number is the longest proper start of the pattern so far that is also its end: after a mismatch there, the pattern can slide on and keep that many characters matched.\")\n\ndef prefix():\n    s = plain(input(\"words, separated by spaces (2 to 10)> \"))\n    if s == \"\":\n        print(\"Cancelled.\")\n        return\n    ws = []\n    w = \"\"\n    for c in s + \" \":\n        if c == \" \":\n            if w != \"\":\n                ws = ws + [w]\n            w = \"\"\n        else:\n            w = w + c\n    if len(ws) < 2 or len(ws) > 10:\n        print(\"Type 2 to 10 words.\")\n        return\n    p = finder.common_prefix(ws)\n    if p == \"\":\n        print(\"They have no common start.\")\n    else:\n        print(\"Longest common start: '\" + p + \"' (\" + str(len(p)) + \" characters).\")\n\ndef new_text(text):\n    s = plain(input(\"text (1 to 120 characters)> \"))\n    if s == \"\":\n        print(\"Cancelled.\")\n        return text\n    if len(s) > 120:\n        print(\"A text is up to 120 characters.\")\n        return text\n    print(\"A text of \" + str(len(s)) + \" characters.\")\n    return s\n\ntext = \"she sells sea shells by the sea shore; the shells she sells are sea shells for sure\"\nprint(\"== Text search ==\")\nprint(\"Every place a pattern occurs: naive, Knuth-Morris-Pratt and Boyer-Moore-Horspool, comparisons counted.\")\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) search  2) KMP table  3) common start of words  4) new text  5) quit\")\n    choice = plain(input(\"choice> \"))\n    if choice == \"1\":\n        search(text)\n    elif choice == \"2\":\n        table()\n    elif choice == \"3\":\n        prefix()\n    elif choice == \"4\":\n        text = new_text(text)\n    elif choice == \"5\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 5.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "finder.eml",
      "eml": "# P090 text search - every place a pattern occurs in a text, found three\n# ways, each counting the character comparisons it makes, and the longest\n# common prefix of a list of words.\n\ndef naive(text, pat):\n    # The corpus case substring-search: try every starting place, comparing\n    # left to right until a character differs. [places, comparisons].\n    len(text) => n\n    len(pat) => m\n    [] => found\n    0 => count\n    for i in [0:n - m]:\n        0 => j\n        True => going\n        while going and j < m:\n            count + 1 => count\n            if text[i + j] == pat[j]:\n                j + 1 => j\n            else:\n                False => going\n        if j == m:\n            found + [i] => found\n    return [found, count]\n\ndef failure(pat):\n    # Knuth-Morris-Pratt's table: for each prefix of the pattern, the length\n    # of the longest proper prefix that is also its suffix. A mismatch then\n    # says how far the pattern can slide without missing a match.\n    len(pat) => m\n    [0] * m => f\n    0 => k\n    for q in [1:m - 1]:\n        while k > 0 and pat[k] != pat[q]:\n            f[k - 1] => k\n        if pat[k] == pat[q]:\n            k + 1 => k\n        k => f[q]\n    return f\n\ndef kmp(text, pat):\n    # Never looks at a text character twice going forward: on a mismatch\n    # the pattern slides by the table instead. [places, comparisons]; the\n    # comparisons are at most 2n.\n    failure(pat) => f\n    len(pat) => m\n    [] => found\n    0 => count\n    0 => q\n    for i in [0:len(text) - 1]:\n        True => more\n        while more:\n            count + 1 => count\n            if text[i] == pat[q]:\n                False => more\n                q + 1 => q\n            elif q > 0:\n                f[q - 1] => q\n            else:\n                False => more\n        if q == m:\n            found + [i - m + 1] => found\n            f[q - 1] => q\n    return [found, count]\n\ndef horspool(text, pat):\n    # Boyer-Moore-Horspool: compare from the pattern's last character\n    # backwards, and after each try slide by how far the text character\n    # under the pattern's end is from the end of the pattern (or the whole\n    # length if it does not occur in the first m - 1). [places, comparisons].\n    len(text) => n\n    len(pat) => m\n    {} => shift\n    for j in [0:m - 2]:\n        m - 1 - j => shift[pat[j]]\n    [] => found\n    0 => count\n    0 => i\n    while i <= n - m:\n        m - 1 => j\n        True => going\n        while going and j >= 0:\n            count + 1 => count\n            if text[i + j] == pat[j]:\n                j - 1 => j\n            else:\n                False => going\n        if j < 0:\n            found + [i] => found\n        text[i + m - 1] => c\n        if c in shift:\n            i + shift[c] => i\n        else:\n            i + m => i\n    return [found, count]\n\ndef common_prefix(words):\n    # The corpus case longest-common-prefix: compare the words column by\n    # column until one ends or two differ.\n    if len(words) == 0:\n        return \"\"\n    words[0] => first\n    0 => k\n    while True:\n        if k >= len(first):\n            return first\n        for w in words:\n            if k >= len(w) or w[k] != first[k]:\n                return first[0:k]\n        k + 1 => k\n",
      "python": "def naive(text, pat):\n    n = len(text)\n    m = len(pat)\n    found = []\n    count = 0\n    for i in range(0, n - m+1):\n        j = 0\n        going = True\n        while going and j < m:\n            count = count + 1\n            if text[i + j] == pat[j]:\n                j = j + 1\n            else:\n                going = False\n        if j == m:\n            found = found + [i]\n    return [found, count]\n\ndef failure(pat):\n    m = len(pat)\n    f = [0] * m\n    k = 0\n    for q in range(1, m):\n        while k > 0 and pat[k] != pat[q]:\n            k = f[k - 1]\n        if pat[k] == pat[q]:\n            k = k + 1\n        f[q] = k\n    return f\n\ndef kmp(text, pat):\n    f = failure(pat)\n    m = len(pat)\n    found = []\n    count = 0\n    q = 0\n    for i in range(0, len(text)):\n        more = True\n        while more:\n            count = count + 1\n            if text[i] == pat[q]:\n                more = False\n                q = q + 1\n            elif q > 0:\n                q = f[q - 1]\n            else:\n                more = False\n        if q == m:\n            found = found + [i - m + 1]\n            q = f[q - 1]\n    return [found, count]\n\ndef horspool(text, pat):\n    n = len(text)\n    m = len(pat)\n    shift = {}\n    for j in range(0, m - 2+1):\n        shift[pat[j]] = m - 1 - j\n    found = []\n    count = 0\n    i = 0\n    while i <= n - m:\n        j = m - 1\n        going = True\n        while going and j >= 0:\n            count = count + 1\n            if text[i + j] == pat[j]:\n                j = j - 1\n            else:\n                going = False\n        if j < 0:\n            found = found + [i]\n        c = text[i + m - 1]\n        if c in shift:\n            i = i + shift[c]\n        else:\n            i = i + m\n    return [found, count]\n\ndef common_prefix(words):\n    if len(words) == 0:\n        return \"\"\n    first = words[0]\n    k = 0\n    while True:\n        if k >= len(first):\n            return first\n        for w in words:\n            if k >= len(w) or w[k] != first[k]:\n                return first[0:k]\n        k = k + 1\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n1\nxxxxxxxxxxxxxxxxxxxxx\n1\n\n2\n\n3\none\n3\nw w w w w w w w w w w\n4\nyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\n4\n\n5\n",
      "screen": "== Text search ==\nEvery place a pattern occurs: naive, Knuth-Morris-Pratt and Boyer-Moore-Horspool, comparisons counted.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 0\nPick a number from 1 to 5.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> x\nPick a number from 1 to 5.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 1\npattern (1 to 20 characters)> xxxxxxxxxxxxxxxxxxxxx\nA pattern is up to 20 characters.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 1\npattern (1 to 20 characters)> \nCancelled.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 2\npattern (1 to 20 characters)> \nCancelled.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 3\nwords, separated by spaces (2 to 10)> one\nType 2 to 10 words.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 3\nwords, separated by spaces (2 to 10)> w w w w w w w w w w w\nType 2 to 10 words.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 4\ntext (1 to 120 characters)> yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\nA text is up to 120 characters.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 4\ntext (1 to 120 characters)> \nCancelled.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "1\nshe\n1\nshells\n1\nsea shore\n1\nxyz\n2\nabacabab\n2\naaaa\n3\nflower flow flight\n3\ndog cat\n4\naaaaaaaaaaaaaaaaaaaaaaaaaaaaab\n1\naaab\n5\n",
      "screen": "== Text search ==\nEvery place a pattern occurs: naive, Knuth-Morris-Pratt and Boyer-Moore-Horspool, comparisons counted.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 1\npattern (1 to 20 characters)> she\n  she sells sea shells by the sea shore; the shells she sells are sea shells for sure\n  ^             ^                            ^      ^                 ^\n'she' occurs 5 times, starting at character 0 14 43 50 68 (counting from 0).\nAll three methods find the same places.\nComparisons: naive 104, KMP 95 (never more than 2 x 83), Horspool 47.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 1\npattern (1 to 20 characters)> shells\n  she sells sea shells by the sea shore; the shells she sells are sea shells for sure\n                ^                            ^                        ^\n'shells' occurs 3 times, starting at character 14 43 68 (counting from 0).\nAll three methods find the same places.\nComparisons: naive 111, KMP 97 (never more than 2 x 83), Horspool 36.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 1\npattern (1 to 20 characters)> sea shore\n  she sells sea shells by the sea shore; the shells she sells are sea shells for sure\n                              ^\n'sea shore' occurs 1 time, starting at character 28 (counting from 0).\nAll three methods find the same places.\nComparisons: naive 110, KMP 96 (never more than 2 x 83), Horspool 24.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 1\npattern (1 to 20 characters)> xyz\n  she sells sea shells by the sea shore; the shells she sells are sea shells for sure\n'xyz' does not occur.\nAll three methods find the same places.\nComparisons: naive 81, KMP 83 (never more than 2 x 83), Horspool 27.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 2\npattern (1 to 20 characters)> abacabab\n  pattern a b a c a b a b\n  table   0 0 1 0 1 2 3 2\nEach number is the longest proper start of the pattern so far that is also its end: after a mismatch there, the pattern can slide on and keep that many characters matched.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 2\npattern (1 to 20 characters)> aaaa\n  pattern a a a a\n  table   0 1 2 3\nEach number is the longest proper start of the pattern so far that is also its end: after a mismatch there, the pattern can slide on and keep that many characters matched.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 3\nwords, separated by spaces (2 to 10)> flower flow flight\nLongest common start: 'fl' (2 characters).\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 3\nwords, separated by spaces (2 to 10)> dog cat\nThey have no common start.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 4\ntext (1 to 120 characters)> aaaaaaaaaaaaaaaaaaaaaaaaaaaaab\nA text of 30 characters.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 1\npattern (1 to 20 characters)> aaab\n  aaaaaaaaaaaaaaaaaaaaaaaaaaaaab\n                            ^\n'aaab' occurs 1 time, starting at character 26 (counting from 0).\nAll three methods find the same places.\nComparisons: naive 108, KMP 56 (never more than 2 x 30), Horspool 30.\n\n1) search  2) KMP table  3) common start of words  4) new text  5) quit\nchoice> 5\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "substring-search",
      "caseId": "106-substring-search",
      "title": "Substring search (all positions)"
    },
    {
      "slug": "longest-common-prefix",
      "caseId": "102-longest-common-prefix",
      "title": "Longest common prefix"
    }
  ],
  "updated": "2026-10-15"
}
