Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Compare text with std.diff

std.diff finds what changed between two versions of a text and reports it as edit operations: runs that stayed equal, were deleted, or were inserted.

Show a unified diff

std.diff.lines compares line by line. unified formats the result the way diff -u does, with the given number of unchanged lines around each change:

import std.diff

def main() -> Result[(), AllocError | IoError]:
    old = "name = \"stats\"\nversion = \"0.1.0\"\nauthors = []\nlicense = \"MIT\"\n"
    new = "name = \"stats\"\nversion = \"0.2.0\"\nauthors = []\nlicense = \"MIT\"\nreadme = \"README.md\"\n"
    changes = std.diff.lines(old, new)?
    print(changes.has_changes())?
    print(changes.unified(1, "plenty.toml", "plenty.toml (edited)")?)?
    Ok(())
True
--- plenty.toml
+++ plenty.toml (edited)
@@ -1,4 +1,5 @@
 name = "stats"
-version = "0.1.0"
+version = "0.2.0"
 authors = []
 license = "MIT"
+readme = "README.md"

A hunk header @@ -1,4 +1,5 @@ says that four lines starting at line 1 of the old text became five lines starting at line 1 of the new one. Equal texts give empty text, so check has_changes first when that matters.

Walk the operations

words splits text into runs of whitespace and runs of other characters, and chars into single characters. Each Op gives a range of old tokens and a range of new tokens; a deletion has an empty new range and an insertion an empty old range:

import std.diff
from std.diff import Kind

def main() -> Result[(), AllocError | IoError]:
    changes = std.diff.words("the quick brown fox", "the quick red fox jumps")?
    old = changes.old_tokens()
    new = changes.new_tokens()
    for op in changes.ops():
        match op.kind:
            case Kind.Equal:
                pass
            case Kind.Delete:
                for index in range(op.old_start, op.old_end):
                    print("removed", str.repr(old[index])?)?
            case Kind.Insert:
                for index in range(op.new_start, op.new_end):
                    print("added", str.repr(new[index])?)?
    Ok(())
removed "brown"
added "red"
added " "
added "jumps"

ops returns a reference, so bind the TextDiff to a name before calling it. std.diff.diff compares any two lists whose elements can be dictionary keys, and std.diff.diff_bytes compares Bytes.

The default algorithm finds the fewest deletions and insertions. Passing Algorithm.Patience as the last argument anchors the comparison on lines that occur once in each text, which often reads better for source code.

Finding the fewest edits takes time quadratic in the input size when two large inputs have little in common. For large or untrusted input, pass a cost limit such as std.diff.lines(old, new, max_cost=Some(512)): the work stays roughly linear and the operations still turn the old text into the new one, but may include more deletions and insertions than necessary. See the std.diff reference for both algorithms, the cost limit, and their costs.