examples/diff/
Un programma che confronta due versioni di un testo e trova la sequenza più lunga comune a entrambe.
- 254 righe
La sequenza può includere elementi separati, purché compaiano nello stesso ordine. Il programma calcola il risultato in una tabella, rappresentata da un array di array, cioè sequenze annidate, e compilata riga per riga. È una delle tecniche usate dagli strumenti di diff per individuare le differenze tra file.
Eseguilo
the first line
- the second line
+ a new line
the third line
2
a
+ x
b
c
1
0
+ only
- only
0
exit 0 · 14 righe
Il programma
# What changed between two versions — the longest common subsequence, and
# the table it is computed in.
#
# Corpus and regression material (CLAUDE.md §9). It closes no measured form
# gap. It is here because it is the one algorithm in this corpus that fills a
# TABLE rather than walking a list: a grid of `[[i64]]` where every cell is
# decided by the three cells above and left of it, and where a reader who is
# off by one anywhere gets an answer that is plausible and wrong.
#
# `examples/board/` also holds a `[[i64]]`, and the difference is the point:
# there the grid is the subject and the program reads it, here the grid is
# scaffolding and the program's answer is a list. That is the shape behind
# every version-control tool anybody has used.
#
# WHY THE LCS AND NOT A SIMPLER DIFF. A line-by-line comparison finds
# CHANGES; only a longest-common-subsequence finds the change that a person
# would have made. Given `a b c` and `a x b c`, a naive walk reports three
# lines changed from position 2 on; the LCS reports one line inserted. The
# tests below name that exact pair, because it is the whole reason the table
# exists.
#
# WHAT IS DELIBERATELY MISSING: any notion of a hunk, a context line, or the
# `@@` header. Those are a FORMAT, and this program is about the algorithm —
# adding the format would double the file and test nothing new.
variant Edit
kept
line: str
added
line: str
removed
line: str
# The table: `same[i][j]` is the length of the longest common subsequence of
# the first `i` lines of `old` and the first `j` of `new`. One row longer and
# one column wider than the inputs, so the empty prefixes have a cell and no
# branch is needed for them — which is the trick that makes this readable.
function table(old: [str], new: [str]) -> [[i64]]
same: [[i64]] @= []
i: i64 @= 0
while i <= old.len()
row: [i64] @= []
j: i64 @= 0
while j <= new.len()
row @ row.push(0)
j @ j + 1
same @ same.push(row)
i @ i + 1
i @ 1
while i <= old.len()
j: i64 @= 1
while j <= new.len()
if old[i - 1] == new[j - 1]
same[i][j] @ same[i - 1][j - 1] + 1
else
left = same[i][j - 1]
up = same[i - 1][j]
same[i][j] @ if left > up
left
else
up
j @ j + 1
i @ i + 1
return same
# The edits, walked BACKWARDS out of the table and then turned round. The
# direction matters: forwards there is no way to know which of two equal
# cells the answer came from, and backwards the table itself says.
function edits(old: [str], new: [str]) -> [Edit]
same = table(old: old, new: new)
backwards: [Edit] @= []
i: i64 @= old.len()
j: i64 @= new.len()
while i > 0 || j > 0
if i > 0 && j > 0 && old[i - 1] == new[j - 1]
backwards @ backwards.push(.kept(line: old[i - 1]))
i @ i - 1
j @ j - 1
continue
# The tie is broken toward `added` on purpose: with `a` against
# `b`, reporting the addition first and the removal second is what
# every diff tool does, and a program that chose the other way would
# print the same edits in a surprising order.
if j > 0 && (i == 0 || same[i][j - 1] >= same[i - 1][j])
backwards @ backwards.push(.added(line: new[j - 1]))
j @ j - 1
continue
backwards @ backwards.push(.removed(line: old[i - 1]))
i @ i - 1
out: [Edit] @= []
at: i64 @= backwards.len() - 1
while at >= 0
out @ out.push(backwards[at])
at @ at - 1
return out
function common_length(old: [str], new: [str]) -> i64
return table(old: old, new: new)[old.len()][new.len()]
function shown(one: Edit) -> str
return match one
.kept k => " " + k.line
.added a => "+ " + a.line
.removed r => "- " + r.line
function rendered(old: [str], new: [str]) -> str
rows: [str] @= []
for one in edits(old: old, new: new)
rows @ rows.push(shown(one))
return join(rows, "\n")
function counted(old: [str], new: [str], of: str) -> i64
total: i64 @= 0
for one in edits(old: old, new: new)
if kind_of(one) == of
total @ total + 1
return total
function kind_of(one: Edit) -> str
return match one
.kept _ => "kept"
.added _ => "added"
.removed _ => "removed"
function main()
before: [str] = ["the first line", "the second line", "the third line"]
after: [str] = ["the first line", "a new line", "the third line"]
print(rendered(old: before, new: after))
print(common_length(old: before, new: after))
# The pair that is the whole reason the table exists: one insertion, not
# three changed lines.
print(rendered(old: ["a", "b", "c"], new: ["a", "x", "b", "c"]))
print(counted(old: ["a", "b", "c"], new: ["a", "x", "b", "c"], of: "added"))
print(counted(old: ["a", "b", "c"], new: ["a", "x", "b", "c"], of: "removed"))
empty: [str] = []
print(rendered(old: empty, new: ["only"]))
print(rendered(old: ["only"], new: empty))
print(common_length(old: empty, new: empty))
test "an insertion is one insertion and not three changes"
# The naive walk reports every line from position 2 on as changed. The
# LCS reports one line added, which is what a person would have written.
old: [str] = ["a", "b", "c"]
new: [str] = ["a", "x", "b", "c"]
assert counted(old: old, new: new, of: "added") == 1
assert counted(old: old, new: new, of: "removed") == 0
assert counted(old: old, new: new, of: "kept") == 3
assert common_length(old: old, new: new) == 3
test "a deletion is the same thing the other way round"
old: [str] = ["a", "x", "b", "c"]
new: [str] = ["a", "b", "c"]
assert counted(old: old, new: new, of: "removed") == 1
assert counted(old: old, new: new, of: "added") == 0
assert counted(old: old, new: new, of: "kept") == 3
test "two identical files have no edits at all"
same: [str] = ["a", "b", "c"]
assert counted(old: same, new: same, of: "added") == 0
assert counted(old: same, new: same, of: "removed") == 0
assert counted(old: same, new: same, of: "kept") == 3
assert common_length(old: same, new: same) == 3
assert rendered(old: same, new: same) == " a\n b\n c"
test "two files with nothing in common keep nothing"
old: [str] = ["a", "b"]
new: [str] = ["x", "y"]
assert common_length(old: old, new: new) == 0
assert counted(old: old, new: new, of: "kept") == 0
assert counted(old: old, new: new, of: "added") == 2
assert counted(old: old, new: new, of: "removed") == 2
test "an empty side is all additions or all removals"
empty: [str] = []
assert rendered(old: empty, new: ["one", "two"]) == "+ one\n+ two"
assert rendered(old: ["one", "two"], new: empty) == "- one\n- two"
assert rendered(old: empty, new: empty) == ""
assert common_length(old: empty, new: ["a"]) == 0
test "the common length is symmetric even though the edits are not"
old: [str] = ["a", "b", "c", "d"]
new: [str] = ["b", "d", "e"]
assert common_length(old: old, new: new) == common_length(old: new, new: old)
assert common_length(old: old, new: new) == 2
# The edits are NOT symmetric: what is added one way is removed the other.
assert counted(old: old, new: new, of: "removed") == counted(old: new, new: old, of: "added")
assert counted(old: old, new: new, of: "added") == counted(old: new, new: old, of: "removed")
test "every kept line appears in both files, and the counts add up"
# The property that makes the answer a diff rather than a list: applying
# the edits to `old` must produce `new`.
old: [str] = ["one", "two", "three", "four"]
new: [str] = ["one", "three", "3.5", "four", "five"]
rebuilt: [str] @= []
for one in edits(old: old, new: new)
match one
.kept k => rebuilt @ rebuilt.push(k.line)
.added a => rebuilt @ rebuilt.push(a.line)
.removed _ => rebuilt @ rebuilt
assert rebuilt == new
# And the other direction: dropping the additions gives back `old`.
original: [str] @= []
for one in edits(old: old, new: new)
match one
.kept k => original @ original.push(k.line)
.removed r => original @ original.push(r.line)
.added _ => original @ original
assert original == old
test "a repeated line is matched once per occurrence and no more"
old: [str] = ["a", "a", "b"]
new: [str] = ["a", "b"]
assert common_length(old: old, new: new) == 2
assert counted(old: old, new: new, of: "removed") == 1
assert counted(old: old, new: new, of: "kept") == 2
test "the table is one row and one column bigger than the inputs"
# The trick that removes every empty-prefix branch, asserted so that a
# later simplification cannot quietly undo it.
old: [str] = ["a", "b"]
new: [str] = ["x"]
grid = table(old: old, new: new)
assert grid.len() == 3
assert grid[0].len() == 2
assert grid[0][0] == 0
assert grid[0][1] == 0
assert grid[1][0] == 0
impronta sha256 d67171914763 · 2 file · index.json
Successivo: dates. Tutti gli esempi. ← Precedente: checksum
Eseguine uno: i quattro comandi. Impara il linguaggio, oppure leggi la specifica completa in un solo file.