text.diff
Comparing two texts line by line, printing the difference as a unified diff and measuring how alike they are.
import text.diff
The diff package finds what changed between two texts. It compares them line by line, finds the shortest list of lines to remove and add, and can print the result as a unified diff, the format of diff -u and git diff: a --- and a +++ header, then hunks that start with @@ -1,3 +1,4 @@ and mark lines with , - and +. The output follows Python's difflib.unified_diff exactly, so tools that read one read the other. Use it for a test that compares generated output with an expected file, for a build step that shows what a generator changed, and for an update check that asks whether anything is different.
There are two levels. Unified and UnifiedText give the finished lines; Opcodes gives the edit script itself, a list of Opcode records that say "lines a[i1:i2] were replaced by b[j1:j2]", for a program that wants to draw the difference its own way. Ratio and RatioText give a number between 0 and 1 for how alike two inputs are.
What people get wrong:
- Expecting nothing back to mean an error. When both inputs are equal,
Unifiedreturns an empty list andUnifiedTextan empty string, not even a header. That is how you test for "no difference". - Passing text where lines are expected.
UnifiedtakesVector<str>; split a text withLinesfirst, or callUnifiedText.Linesdrops the last empty piece after a final newline and a trailing\ron each line, so files with Windows line ends compare equal to files with Unix ones. - Reading the ratio as an edit distance.
Ratiois2*M/T, whereMis the number of lines the two share in order andTthe total of both lengths. It is built from a longest common subsequence, so it can be a little higher thandifflib.SequenceMatcher.ratio(), which stops early on some inputs. - Diffing huge, completely different inputs. The search stops after 3000 edits and then treats what is left as one block replaced by another. The result is still a correct diff, only not the shortest.
- Forgetting to free.
Lines,Unified,OpcodesandGroupedOpcodesreturn vectors the caller frees (and each inner vector ofGroupedOpcodes).
Showing what changed between two files
Read both files, hand them to UnifiedText with their names for the header, and print the answer when it is not empty. The third argument after the names is the number of unchanged lines to show around each change (3 is the usual choice, 0 shows only the changes).
import text.diff
import fs.file
import fs.path
import os.temp
func main:
work := temp.Directory()
defer temp.RemoveDir(work)
left := path.Join(work, "config.old")
right := path.Join(work, "config.new")
file.WriteText(left, "port=80\nhost=a\nmode=safe\n")
file.WriteText(right, "port=8080\nhost=a\nmode=safe\ndebug=1\n")
patch := diff.UnifiedText(file.ReadText(left), file.ReadText(right), "config.old", "config.new", 1)
if patch.len() == 0:
println "the files are the same"
else:
print patch
end
println "same file:", diff.UnifiedText("x\n", "x\n", "a", "b", 3).len() == 0
endSuggesting the closest name
RatioText turns "how alike are these two words" into a number, so a program can answer a typo with "did you mean". The example below tries each option, keeps the one with the highest score and only suggests it when the score is at least 0.6. Ratio does the same for lists of lines: three of the four lines are shared, so the score is 0.75.
import text.diff
func closest(word: str, options: str[]): str:
mut best := ""
mut best_score := 0.0
each option in options:
score := diff.RatioText(word, option)
if score > best_score:
best_score = score
best = option
end
end
ret best
end
func main:
commands := ["start", "stop", "status", "restart", "version"]
each typed in ["starts", "stops", "restarts", "xyz"]:
guess := closest(typed, commands)
score := guess.len() > 0 ? diff.RatioText(typed, guess) : 0.0
if score >= 0.6:
println typed, "->", guess
else:
println typed, "-> no suggestion"
end
end
old := diff.Lines("a\nb\nc\nd\n")
fresh := diff.Lines("a\nb\nx\nd\n")
defer old.free()
defer fresh.free()
println "lines in common:", diff.Ratio(old, fresh)
endTypes
Opcode
struct Opcodea[a_start:a_stop] compared with b[b_start:b_stop]. The range of the second input is empty for a delete, and the range of the first input is empty for an insert.| Name | Type | Description |
|---|---|---|
tag | int = 0 | One of Equal, Replace, Delete and Insert. |
a_start | int = 0 | Index of the first line of the first input this step covers. |
a_stop | int = 0 | Index after the last line of the first input this step covers. |
b_start | int = 0 | Index of the first line of the second input this step covers. |
b_stop | int = 0 | Index after the last line of the second input this step covers. |
import text.diff
import str
func name(tag: int): str:
switch tag:
diff.Equal:
ret "equal"
end
diff.Replace:
ret "replace"
end
diff.Delete:
ret "delete"
end
else:
ret "insert"
end
end
end
func main:
a := diff.Lines("one\ntwo\nthree\nfour\n")
b := diff.Lines("one\n2\nthree\nfour\nfive\n")
defer a.free()
defer b.free()
ops := diff.Opcodes(a, b)
defer ops.free()
each op in ops:
println name(op.tag), "a[" + str.FromInt(op.a_start as i64) + ":" + str.FromInt(op.a_stop as i64) + "]", "b[" + str.FromInt(op.b_start as i64) + ":" + str.FromInt(op.b_stop as i64) + "]"
end
groups := diff.GroupedOpcodes(a, b, 0)
println "hunks:", groups.len()
each g in groups:
g.free()
end
groups.free()
endConstants and variables
Equal
const Equal: i32 = 0The example for this item is shown with the ones above: text-diff-Opcodes.salam
Replace
const Replace: i32 = 1The example for this item is shown with the ones above: text-diff-Opcodes.salam
Delete
const Delete: i32 = 2The example for this item is shown with the ones above: text-diff-Opcodes.salam
Insert
const Insert: i32 = 3The example for this item is shown with the ones above: text-diff-Opcodes.salam
Functions
Opcodes
func Opcodes(a: Vector<str>, b: Vector<str>): Vector<Opcode>a into b: opcodes that follow each other and together cover both inputs from start to end. Neighbouring removals and additions are joined into one Replace. The caller frees the vector.The example for this item is shown with the ones above: text-diff-Opcodes.salam
GroupedOpcodes
func GroupedOpcodes(a: Vector<str>, b: Vector<str>, context: int): Vector<Vector<Opcode>>context unchanged lines on either side of a change, and no hunk at all when the inputs are equal. A negative context counts as 0. The caller frees the outer vector and every inner one.The example for this item is shown with the ones above: text-diff-Opcodes.salam
Unified
func Unified(a: Vector<str>, b: Vector<str>, from_name: str, to_name: str, context: int): Vector<str>--- and +++ headers with the two names, then each hunk with its @@ line. The lines have no line terminator. Equal inputs give an empty vector. The caller frees the vector.import text.diff
func main:
old := diff.Lines("red\ngreen\nblue\n")
fresh := diff.Lines("red\nyellow\nblue\nblack\n")
defer old.free()
defer fresh.free()
lines := diff.Unified(old, fresh, "old.txt", "new.txt", 1)
defer lines.free()
each line in lines:
println line
end
print diff.UnifiedText("a\nb\n", "a\nc\n", "x", "y", 0)
endLines
func Lines(text: str): Vector<str>\n, dropping the empty piece after a final newline and a \r at the end of a line. An empty text has no lines. The caller frees the vector.The example for this item is shown with the ones above: text-diff-Unified.salam
UnifiedText
func UnifiedText(a: str, b: str, from_name: str, to_name: str, context: int): strUnified for two texts, as one string with a newline after every line; empty when the texts have the same lines.The example for this item is shown with the ones above: text-diff-Unified.salam
Ratio
func Ratio(a: Vector<str>, b: Vector<str>): f64import text.diff
func main:
println diff.RatioText("kitten", "sitting")
println diff.RatioText("same", "same"), diff.RatioText("abc", "xyz")
a := diff.Lines("a\nb\nc\nd\n")
b := diff.Lines("a\nb\nx\nd\n")
defer a.free()
defer b.free()
println diff.Ratio(a, b)
endRatioText
func RatioText(a: str, b: str): f64The example for this item is shown with the ones above: text-diff-Ratio.salam