package reconcile // OpKind is the kind of one run in an edit script. type OpKind int const ( // Equal: A[AStart:AEnd] and B[BStart:BEnd] are the same words. Equal OpKind = iota // Replace: different words on both sides. Replace // Delete: words only in A. Delete // Insert: words only in B. Insert ) // Op is one run of the edit script. The ops of a Diff tile both inputs: each op starts // where the previous one ended, in A and in B. type Op struct { Kind OpKind AStart, AEnd int BStart, BEnd int } // maxEdits bounds the Myers search. Memory grows with the square of the edit // distance, so two texts that share almost nothing (one engine produced garbage) // are reported as one Replace instead of spending gigabytes proving it. A real // hour disagrees in roughly 400 places, i.e. about 800 edits, far below this. const maxEdits = 3000 // Diff returns a minimal edit script from a to b (Myers, O(ND)). Adjacent deletions // and insertions are reported as one Replace. func Diff(a, b []string) []Op { pre := 0 for pre < len(a) && pre < len(b) && a[pre] == b[pre] { pre++ } suf := 0 for suf < len(a)-pre && suf < len(b)-pre && a[len(a)-1-suf] == b[len(b)-1-suf] { suf++ } midA, midB := a[pre:len(a)-suf], b[pre:len(b)-suf] steps, ok := myers(midA, midB) if !ok { steps = steps[:0] for range midA { steps = append(steps, stepDel) } for range midB { steps = append(steps, stepIns) } } var ops []Op ai, bi := 0, 0 emit := func(k OpKind, da, db int) { if da == 0 && db == 0 { return } if k == Equal && len(ops) > 0 && ops[len(ops)-1].Kind == Equal { ops[len(ops)-1].AEnd += da ops[len(ops)-1].BEnd += db } else { ops = append(ops, Op{Kind: k, AStart: ai, AEnd: ai + da, BStart: bi, BEnd: bi + db}) } ai += da bi += db } emit(Equal, pre, pre) for i := 0; i < len(steps); { if steps[i] == stepEq { j := i for j < len(steps) && steps[j] == stepEq { j++ } emit(Equal, j-i, j-i) i = j continue } dels, inss := 0, 0 for i < len(steps) && steps[i] != stepEq { if steps[i] == stepDel { dels++ } else { inss++ } i++ } switch { case dels > 0 && inss > 0: emit(Replace, dels, inss) case dels > 0: emit(Delete, dels, 0) default: emit(Insert, 0, inss) } } emit(Equal, suf, suf) return ops } type step int8 const ( stepEq step = iota stepDel stepIns ) // myers returns one step per element of the edit script, in order. ok is false when // the edit distance exceeds maxEdits. func myers(a, b []string) ([]step, bool) { n, m := len(a), len(b) if n == 0 && m == 0 { return nil, true } limit := min(n+m, maxEdits) off := limit + 1 v := make([]int32, 2*limit+3) var trace [][]int32 // trace[d] is v as it stood at the start of round d, for k in [-d, d] for d := 0; d <= limit; d++ { snap := make([]int32, 2*d+1) for k := -d; k <= d; k++ { snap[k+d] = v[off+k] } trace = append(trace, snap) for k := -d; k <= d; k += 2 { var x int if k == -d || (k != d && v[off+k-1] < v[off+k+1]) { x = int(v[off+k+1]) } else { x = int(v[off+k-1]) + 1 } y := x - k for x < n && y < m && a[x] == b[y] { x++ y++ } v[off+k] = int32(x) if x >= n && y >= m { return backtrack(trace, n, m, d), true } } } return nil, false } func backtrack(trace [][]int32, n, m, d int) []step { var rev []step x, y := n, m for ; d >= 0; d-- { snap := trace[d] at := func(k int) int { return int(snap[k+d]) } k := x - y var prevK int if k == -d || (k != d && at(k-1) < at(k+1)) { prevK = k + 1 } else { prevK = k - 1 } prevX := 0 if d > 0 { prevX = at(prevK) } prevY := prevX - prevK for x > prevX && y > prevY { rev = append(rev, stepEq) x-- y-- } if d > 0 { if x == prevX { rev = append(rev, stepIns) } else { rev = append(rev, stepDel) } } x, y = prevX, prevY } out := make([]step, len(rev)) for i, s := range rev { out[len(rev)-1-i] = s } return out }