{
  "id": "P043",
  "slug": "line-diff",
  "key": "P043-line-diff",
  "title": "Line diff",
  "summary": "Two texts compared line by line: the longest run of lines they share, in order, stays, and everything else is shown as removed from the first or added in the second - in full with line numbers on both sides, or only the changes with a line of context around each. The texts can be swapped to see the change the other way.",
  "entry": "main.eml",
  "ui": "terminal",
  "readme": "# P043 - Line diff\n\nTwo texts, A and B, compared line by line. The longest run of lines they\nshare, in order, stays; everything else is shown as removed from A or added\nin B - the whole texts with the line numbers on both sides, or only the\nchanges with a line of context around each. A and B can be swapped to see\nthe change the other way round.\n\n- `main.eml` - the menu, typing a text with its checks, the sample, and the\n  differences on screen\n- `diff.eml` - the table of common lines, the walk that turns it into a\n  difference, and which lines to show\n\nHow each part works:\n\n- The table is the one of the corpus case `longest-common-subsequence`, with\n  lines in place of characters, filled from the ends: entry (i, j) is the\n  most lines that A from line i and B from line j can keep in common, in\n  order. The corpus case walks its table backwards to recover the common\n  part; filled from the ends, this one can be walked forwards, which is the\n  order a difference is read in.\n- The walk keeps a line both texts can keep; otherwise it drops the line of\n  A if that loses nothing, and adds the line of B if it does. So the number\n  of lines kept is the largest possible, and in every changed stretch the\n  removed lines come before the added ones.\n- \"Changes only\" shows every change and one unchanged line on each side of\n  it; `...` stands for the lines left out.\n- Lines are compared exactly, spaces at the end aside.\n\nWhat is checked: at most 30 lines of up to 60 characters per text; a line\nholding only a dot ends the text, so an empty line can be part of it. An\nempty menu answer is not a choice.\n\nSessions: `sessions/basic.in` compares the sample - two versions of a to-do\nlist, three lines removed, three added, three the same - both ways round;\nthen types two versions of a recipe, ten and eleven lines long, and shows\nonly the changes: three places, with `...` between them.\n`sessions/bad-input.in` gives menu choices 0 and x, compares two empty\ntexts, types a line of 61 characters, makes A and B the same three lines\n(an empty one among them), then an empty B - everything removed, and after\nthe swap everything added - and types 31 lines, which stops at 30.\n\nBuilt on the verified corpus case `longest-common-subsequence` (the dynamic\nprogramming table and the walk that recovers the common part).\n",
  "modules": [
    {
      "name": "main.eml",
      "eml": "# P043 line diff: two texts, A and B, compared line by line. The longest run\n# of lines they share, in order, stays; everything else is shown as removed\n# from A or added in B - the whole texts, or only the changes with a line of\n# context around each.\nimport diff\n\n30 => most_lines\n60 => longest_line\n\ndef trim_right(s):\n    len(s) => j\n    while j > 0 and s[j - 1] == \" \":\n        j - 1 => j\n    return s[0:j]\n\ndef right(s, width):\n    while len(s) < width:\n        \" \" + s => s\n    return s\n\ndef lines_text(n):\n    if n == 1:\n        return \"1 line\"\n    return str(n) + \" lines\"\n\ndef read_text(name):\n    (\"Type text \" + name + \" line by line, at most \" + str(most_lines) + \" lines of up to \" + str(longest_line) + \" characters; a line with only a dot ends it.\") ^0\n    [] => lines\n    while True:\n        trim_right(input(name + str(len(lines) + 1) + \"> \")) => line\n        if line == \".\":\n            (name + \" has \" + lines_text(len(lines)) + \".\") ^0\n            return lines\n        if len(line) > longest_line:\n            (\"That line has \" + str(len(line)) + \" characters; the most is \" + str(longest_line) + \". Type it again, shorter.\") ^0\n        elif len(lines) == most_lines:\n            (\"That makes \" + str(most_lines) + \" lines, the most; \" + name + \" ends here.\") ^0\n            return lines\n        else:\n            lines + [line] => lines\n\ndef show_difference(a, b, context):\n    if len(a) == 0 and len(b) == 0:\n        \"Both texts are empty.\" ^0\n        return 0\n    diff.difference(a, b) => d\n    diff.counts(d) => c\n    diff.shown_lines(d, context) => show\n    if context != -1 and c[1] == 0 and c[2] == 0:\n        (\"The texts are the same: \" + lines_text(c[0]) + \".\") ^0\n        return 0\n    \"    A   B\" ^0\n    False => gap\n    for k in [0:len(d) - 1]:\n        if show[k]:\n            if gap:\n                \"   ...\" ^0\n            False => gap\n            d[k] => x\n            \"\" => na\n            if x[1] > 0:\n                str(x[1]) => na\n            \"\" => nb\n            if x[2] > 0:\n                str(x[2]) => nb\n            trim_right(x[0] + \" \" + right(na, 3) + \" \" + right(nb, 3) + \"  \" + x[3]) ^0\n        else:\n            True => gap\n    if gap:\n        \"   ...\" ^0\n    if c[1] == 0 and c[2] == 0:\n        (\"The texts are the same: \" + lines_text(c[0]) + \".\") ^0\n    else:\n        (lines_text(c[1]) + \" removed, \" + lines_text(c[2]) + \" added, \" + lines_text(c[0]) + \" the same - the longest common part, in order.\") ^0\n    return 0\n\n\"== Line diff ==\" ^0\n\"Compare two texts, A and B, line by line.\" ^0\n[] => a\n[] => b\nTrue => running\nwhile running:\n    \"\" ^0\n    \"1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\" ^0\n    trim_right(input(\"choice> \")) => choice\n    if choice == \"1\":\n        read_text(\"A\") => a\n    elif choice == \"2\":\n        read_text(\"B\") => b\n    elif choice == \"3\":\n        [\"Buy milk\", \"Call the plumber\", \"Pay the electricity bill\", \"Write to Ana\", \"Book the dentist\", \"Water the plants\"] => a\n        [\"Buy milk and bread\", \"Call the plumber\", \"Write to Ana\", \"Water the plants\", \"Book the dentist\", \"Clean the windows\"] => b\n        \"A and B now hold two versions of a to-do list.\" ^0\n    elif choice == \"4\":\n        show_difference(a, b, -1)\n    elif choice == \"5\":\n        show_difference(a, b, 1)\n    elif choice == \"6\":\n        a => t\n        b => a\n        t => b\n        \"A and B swapped.\" ^0\n    elif choice == \"7\":\n        False => running\n    else:\n        \"Pick a number from 1 to 7.\" ^0\n\"Bye.\" ^0\n",
      "python": "import diff\nmost_lines = 30\nlongest_line = 60\n\ndef trim_right(s):\n    j = len(s)\n    while j > 0 and s[j - 1] == \" \":\n        j = j - 1\n    return s[0:j]\n\ndef right(s, width):\n    while len(s) < width:\n        s = \" \" + s\n    return s\n\ndef lines_text(n):\n    if n == 1:\n        return \"1 line\"\n    return str(n) + \" lines\"\n\ndef read_text(name):\n    print(\"Type text \" + name + \" line by line, at most \" + str(most_lines) + \" lines of up to \" + str(longest_line) + \" characters; a line with only a dot ends it.\")\n    lines = []\n    while True:\n        line = trim_right(input(name + str(len(lines) + 1) + \"> \"))\n        if line == \".\":\n            print(name + \" has \" + lines_text(len(lines)) + \".\")\n            return lines\n        if len(line) > longest_line:\n            print(\"That line has \" + str(len(line)) + \" characters; the most is \" + str(longest_line) + \". Type it again, shorter.\")\n        elif len(lines) == most_lines:\n            print(\"That makes \" + str(most_lines) + \" lines, the most; \" + name + \" ends here.\")\n            return lines\n        else:\n            lines = lines + [line]\n\ndef show_difference(a, b, context):\n    if len(a) == 0 and len(b) == 0:\n        print(\"Both texts are empty.\")\n        return 0\n    d = diff.difference(a, b)\n    c = diff.counts(d)\n    show = diff.shown_lines(d, context)\n    if context != -1 and c[1] == 0 and c[2] == 0:\n        print(\"The texts are the same: \" + lines_text(c[0]) + \".\")\n        return 0\n    print(\"    A   B\")\n    gap = False\n    for k in range(0, len(d)):\n        if show[k]:\n            if gap:\n                print(\"   ...\")\n            gap = False\n            x = d[k]\n            na = \"\"\n            if x[1] > 0:\n                na = str(x[1])\n            nb = \"\"\n            if x[2] > 0:\n                nb = str(x[2])\n            print(trim_right(x[0] + \" \" + right(na, 3) + \" \" + right(nb, 3) + \"  \" + x[3]))\n        else:\n            gap = True\n    if gap:\n        print(\"   ...\")\n    if c[1] == 0 and c[2] == 0:\n        print(\"The texts are the same: \" + lines_text(c[0]) + \".\")\n    else:\n        print(lines_text(c[1]) + \" removed, \" + lines_text(c[2]) + \" added, \" + lines_text(c[0]) + \" the same - the longest common part, in order.\")\n    return 0\n\nprint(\"== Line diff ==\")\nprint(\"Compare two texts, A and B, line by line.\")\na = []\nb = []\nrunning = True\nwhile running:\n    print(\"\")\n    print(\"1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\")\n    choice = trim_right(input(\"choice> \"))\n    if choice == \"1\":\n        a = read_text(\"A\")\n    elif choice == \"2\":\n        b = read_text(\"B\")\n    elif choice == \"3\":\n        a = [\"Buy milk\", \"Call the plumber\", \"Pay the electricity bill\", \"Write to Ana\", \"Book the dentist\", \"Water the plants\"]\n        b = [\"Buy milk and bread\", \"Call the plumber\", \"Write to Ana\", \"Water the plants\", \"Book the dentist\", \"Clean the windows\"]\n        print(\"A and B now hold two versions of a to-do list.\")\n    elif choice == \"4\":\n        show_difference(a, b, -1)\n    elif choice == \"5\":\n        show_difference(a, b, 1)\n    elif choice == \"6\":\n        t = a\n        a = b\n        b = t\n        print(\"A and B swapped.\")\n    elif choice == \"7\":\n        running = False\n    else:\n        print(\"Pick a number from 1 to 7.\")\nprint(\"Bye.\")\n"
    },
    {
      "name": "diff.eml",
      "eml": "# P043 line diff - which lines two texts share and how one becomes the\n# other. A text is a list of lines; a difference is a list of\n# [kind, line number in A, line number in B, text], kind \" \" for a line in\n# both, \"-\" for one only in A, \"+\" for one only in B (numbers from 1, 0 where\n# the line is not in that text).\n\ndef common_table(a, b):\n    # t[i][j] is the length of the longest common subsequence of the lines\n    # a[i:] and b[j:] - the corpus case longest-common-subsequence, filled\n    # from the ends so that the walk below can go forwards.\n    len(a) => n\n    len(b) => m\n    [] => t\n    for i in [0:n]:\n        t + [[0] * (m + 1)] => t\n    n - 1 => i\n    while i >= 0:\n        m - 1 => j\n        while j >= 0:\n            if a[i] == b[j]:\n                t[i + 1][j + 1] + 1 => t[i][j]\n            elif t[i + 1][j] >= t[i][j + 1]:\n                t[i + 1][j] => t[i][j]\n            else:\n                t[i][j + 1] => t[i][j]\n            j - 1 => j\n        i - 1 => i\n    return t\n\ndef difference(a, b):\n    # Walks from the start: a line both texts can keep is kept; otherwise\n    # the line of A goes if dropping it loses nothing, else the line of B is\n    # added - so in each changed stretch the removed lines come first.\n    common_table(a, b) => t\n    0 => i\n    0 => j\n    [] => out\n    while i < len(a) or j < len(b):\n        if i < len(a) and j < len(b) and a[i] == b[j]:\n            out + [[\" \", i + 1, j + 1, a[i]]] => out\n            i + 1 => i\n            j + 1 => j\n        elif j == len(b) or (i < len(a) and t[i + 1][j] >= t[i][j + 1]):\n            out + [[\"-\", i + 1, 0, a[i]]] => out\n            i + 1 => i\n        else:\n            out + [[\"+\", 0, j + 1, b[j]]] => out\n            j + 1 => j\n    return out\n\ndef counts(d):\n    # [kept, removed, added]\n    0 => kept\n    0 => removed\n    0 => added\n    for x in d:\n        if x[0] == \" \":\n            kept + 1 => kept\n        elif x[0] == \"-\":\n            removed + 1 => removed\n        else:\n            added + 1 => added\n    return [kept, removed, added]\n\ndef shown_lines(d, context):\n    # Which entries to show: every change, and up to `context` unchanged\n    # lines on each side of one; -1 means all. Returns a list of True/False.\n    [False] * len(d) => show\n    for k in [0:len(d) - 1]:\n        if context == -1 or d[k][0] != \" \":\n            True => show[k]\n            if context > 0:\n                for e in [1:context]:\n                    if k - e >= 0:\n                        True => show[k - e]\n                    if k + e < len(d):\n                        True => show[k + e]\n    return show\n",
      "python": "def common_table(a, b):\n    n = len(a)\n    m = len(b)\n    t = []\n    for i in range(0, n+1):\n        t = t + [[0] * (m + 1)]\n    i = n - 1\n    while i >= 0:\n        j = m - 1\n        while j >= 0:\n            if a[i] == b[j]:\n                t[i][j] = t[i + 1][j + 1] + 1\n            elif t[i + 1][j] >= t[i][j + 1]:\n                t[i][j] = t[i + 1][j]\n            else:\n                t[i][j] = t[i][j + 1]\n            j = j - 1\n        i = i - 1\n    return t\n\ndef difference(a, b):\n    t = common_table(a, b)\n    i = 0\n    j = 0\n    out = []\n    while i < len(a) or j < len(b):\n        if i < len(a) and j < len(b) and a[i] == b[j]:\n            out = out + [[\" \", i + 1, j + 1, a[i]]]\n            i = i + 1\n            j = j + 1\n        elif j == len(b) or i < len(a) and t[i + 1][j] >= t[i][j + 1]:\n            out = out + [[\"-\", i + 1, 0, a[i]]]\n            i = i + 1\n        else:\n            out = out + [[\"+\", 0, j + 1, b[j]]]\n            j = j + 1\n    return out\n\ndef counts(d):\n    kept = 0\n    removed = 0\n    added = 0\n    for x in d:\n        if x[0] == \" \":\n            kept = kept + 1\n        elif x[0] == \"-\":\n            removed = removed + 1\n        else:\n            added = added + 1\n    return [kept, removed, added]\n\ndef shown_lines(d, context):\n    show = [False] * len(d)\n    for k in range(0, len(d)):\n        if context == -1 or d[k][0] != \" \":\n            show[k] = True\n            if context > 0:\n                for e in range(1, context+1):\n                    if k - e >= 0:\n                        show[k - e] = True\n                    if k + e < len(d):\n                        show[k + e] = True\n    return show\n"
    }
  ],
  "sessions": [
    {
      "name": "bad-input",
      "input": "0\nx\n4\n1\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nfirst\n\nthird\n.\n2\nfirst\n\nthird\n.\n4\n5\n2\n.\n4\n6\n4\n1\nline 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\nline 20\nline 21\nline 22\nline 23\nline 24\nline 25\nline 26\nline 27\nline 28\nline 29\nline 30\nline 31\n7\n",
      "screen": "== Line diff ==\nCompare two texts, A and B, line by line.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 0\nPick a number from 1 to 7.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> x\nPick a number from 1 to 7.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 4\nBoth texts are empty.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 1\nType text A line by line, at most 30 lines of up to 60 characters; a line with only a dot ends it.\nA1> xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nThat line has 61 characters; the most is 60. Type it again, shorter.\nA1> first\nA2> \nA3> third\nA4> .\nA has 3 lines.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 2\nType text B line by line, at most 30 lines of up to 60 characters; a line with only a dot ends it.\nB1> first\nB2> \nB3> third\nB4> .\nB has 3 lines.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 4\n    A   B\n    1   1  first\n    2   2\n    3   3  third\nThe texts are the same: 3 lines.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 5\nThe texts are the same: 3 lines.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 2\nType text B line by line, at most 30 lines of up to 60 characters; a line with only a dot ends it.\nB1> .\nB has 0 lines.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 4\n    A   B\n-   1      first\n-   2\n-   3      third\n3 lines removed, 0 lines added, 0 lines the same - the longest common part, in order.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 6\nA and B swapped.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 4\n    A   B\n+       1  first\n+       2\n+       3  third\n0 lines removed, 3 lines added, 0 lines the same - the longest common part, in order.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 1\nType text A line by line, at most 30 lines of up to 60 characters; a line with only a dot ends it.\nA1> line 1\nA2> line 2\nA3> line 3\nA4> line 4\nA5> line 5\nA6> line 6\nA7> line 7\nA8> line 8\nA9> line 9\nA10> line 10\nA11> line 11\nA12> line 12\nA13> line 13\nA14> line 14\nA15> line 15\nA16> line 16\nA17> line 17\nA18> line 18\nA19> line 19\nA20> line 20\nA21> line 21\nA22> line 22\nA23> line 23\nA24> line 24\nA25> line 25\nA26> line 26\nA27> line 27\nA28> line 28\nA29> line 29\nA30> line 30\nA31> line 31\nThat makes 30 lines, the most; A ends here.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    },
    {
      "name": "basic",
      "input": "3\n4\n6\n4\n1\nHeat the oven to 200 degrees.\nMix the flour and the salt.\nRub in the butter.\nAdd the milk slowly.\nKnead the dough for a minute.\nRoll it out to two centimetres.\nCut out the rounds.\nBrush the tops with milk.\nBake for twelve minutes.\nCool on a rack.\n.\n2\nHeat the oven to 220 degrees.\nMix the flour and the salt.\nRub in the butter.\nAdd the milk slowly.\nKnead the dough for a minute.\nRest it for ten minutes.\nRoll it out to two centimetres.\nCut out the rounds.\nBrush the tops with milk.\nBake for ten to twelve minutes.\nCool on a rack.\n.\n5\n7\n",
      "screen": "== Line diff ==\nCompare two texts, A and B, line by line.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 3\nA and B now hold two versions of a to-do list.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 4\n    A   B\n-   1      Buy milk\n+       1  Buy milk and bread\n    2   2  Call the plumber\n-   3      Pay the electricity bill\n    4   3  Write to Ana\n-   5      Book the dentist\n    6   4  Water the plants\n+       5  Book the dentist\n+       6  Clean the windows\n3 lines removed, 3 lines added, 3 lines the same - the longest common part, in order.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 6\nA and B swapped.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 4\n    A   B\n-   1      Buy milk and bread\n+       1  Buy milk\n    2   2  Call the plumber\n+       3  Pay the electricity bill\n    3   4  Write to Ana\n-   4      Water the plants\n    5   5  Book the dentist\n-   6      Clean the windows\n+       6  Water the plants\n3 lines removed, 3 lines added, 3 lines the same - the longest common part, in order.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 1\nType text A line by line, at most 30 lines of up to 60 characters; a line with only a dot ends it.\nA1> Heat the oven to 200 degrees.\nA2> Mix the flour and the salt.\nA3> Rub in the butter.\nA4> Add the milk slowly.\nA5> Knead the dough for a minute.\nA6> Roll it out to two centimetres.\nA7> Cut out the rounds.\nA8> Brush the tops with milk.\nA9> Bake for twelve minutes.\nA10> Cool on a rack.\nA11> .\nA has 10 lines.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 2\nType text B line by line, at most 30 lines of up to 60 characters; a line with only a dot ends it.\nB1> Heat the oven to 220 degrees.\nB2> Mix the flour and the salt.\nB3> Rub in the butter.\nB4> Add the milk slowly.\nB5> Knead the dough for a minute.\nB6> Rest it for ten minutes.\nB7> Roll it out to two centimetres.\nB8> Cut out the rounds.\nB9> Brush the tops with milk.\nB10> Bake for ten to twelve minutes.\nB11> Cool on a rack.\nB12> .\nB has 11 lines.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 5\n    A   B\n-   1      Heat the oven to 200 degrees.\n+       1  Heat the oven to 220 degrees.\n    2   2  Mix the flour and the salt.\n   ...\n    5   5  Knead the dough for a minute.\n+       6  Rest it for ten minutes.\n    6   7  Roll it out to two centimetres.\n   ...\n    8   9  Brush the tops with milk.\n-   9      Bake for twelve minutes.\n+      10  Bake for ten to twelve minutes.\n   10  11  Cool on a rack.\n2 lines removed, 3 lines added, 8 lines the same - the longest common part, in order.\n\n1) type A  2) type B  3) sample  4) differences  5) changes only  6) swap  7) quit\nchoice> 7\nBye.\n",
      "interpreter": "equal"
    }
  ],
  "builtOn": [
    {
      "slug": "longest-common-subsequence",
      "caseId": "088-longest-common-subsequence",
      "title": "Longest common subsequence"
    }
  ],
  "updated": "2026-10-10"
}
