package prosediff
// Myers O(ND) sequence diff, used at three levels: blocks, words and code
// lines. Sequences are always interned to []int first, so equality is exact
// integer equality and there is no hash-collision risk.
// Op is the role of a run in an edit script.
type Op uint8
const (
// OpEqual marks content present, unchanged, in both revisions.
OpEqual Op = iota
// OpDelete marks content present only in the old revision.
OpDelete
// OpInsert marks content present only in the new revision.
OpInsert
)
func (o Op) String() string {
switch o {
case OpEqual:
return "equal"
case OpDelete:
return "delete"
case OpInsert:
return "insert"
}
return "unknown"
}
// edit is one run of an edit script: n consecutive elements sharing an Op.
type edit struct {
op Op
n int
}
// interner maps arbitrary strings to dense ints so sequence comparison is
// integer equality.
type interner struct {
ids map[string]int
}
func newInterner() *interner { return &interner{ids: make(map[string]int)} }
func (in *interner) id(s string) int {
if v, ok := in.ids[s]; ok {
return v
}
v := len(in.ids) + 1
in.ids[s] = v
return v
}
func (in *interner) all(ss []string) []int {
out := make([]int, len(ss))
for i, s := range ss {
out[i] = in.id(s)
}
return out
}
// diffInts returns the edit script turning a into b, as a coalesced run list.
// The script is a minimal edit script (Myers), with common prefix and suffix
// trimmed first so the expensive part only runs over the changed middle.
func diffInts(a, b []int) []edit {
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++
}
var out []edit
if pre > 0 {
out = append(out, edit{OpEqual, pre})
}
out = append(out, myers(a[pre:len(a)-suf], b[pre:len(b)-suf])...)
if suf > 0 {
out = append(out, edit{OpEqual, suf})
}
return coalesce(out)
}
// vsnap is the V array of one Myers iteration, stored only over the diagonals
// [-d, d] that iteration can reach.
type vsnap struct {
d int
vals []int32
}
func (s vsnap) get(k int) int { return int(s.vals[k+s.d]) }
func myers(a, b []int) []edit {
n, m := len(a), len(b)
switch {
case n == 0 && m == 0:
return nil
case n == 0:
return []edit{{OpInsert, m}}
case m == 0:
return []edit{{OpDelete, n}}
}
maxD := n + m
v := make([]int32, 2*maxD+1)
off := maxD
trace := make([]vsnap, 0, 16)
for d := 0; d <= maxD; d++ {
done := false
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]) // move down: consume one element of b
} else {
x = int(v[off+k-1]) + 1 // move right: consume one element of a
}
y := x - k
for x < n && y < m && a[x] == b[y] {
x++
y++
}
v[off+k] = int32(x)
if x >= n && y >= m {
done = true
break
}
}
snap := vsnap{d: d, vals: make([]int32, 2*d+1)}
for k := -d; k <= d; k += 2 {
snap.vals[k+d] = v[off+k]
}
trace = append(trace, snap)
if done {
return backtrack(trace, n, m)
}
}
panic("prosediff: myers did not converge")
}
// backtrack walks the saved V arrays from the end point back to the origin,
// emitting the edit script in reverse and then flipping it.
func backtrack(trace []vsnap, n, m int) []edit {
var rev []edit
push := func(op Op) {
if len(rev) > 0 && rev[len(rev)-1].op == op {
rev[len(rev)-1].n++
return
}
rev = append(rev, edit{op, 1})
}
x, y := n, m
for d := len(trace) - 1; d > 0; d-- {
prev := trace[d-1]
k := x - y
var prevK int
if k == -d || (k != d && prev.get(k-1) < prev.get(k+1)) {
prevK = k + 1
} else {
prevK = k - 1
}
prevX := prev.get(prevK)
prevY := prevX - prevK
for x > prevX && y > prevY {
push(OpEqual)
x--
y--
}
if x == prevX {
push(OpInsert)
y--
} else {
push(OpDelete)
x--
}
x, y = prevX, prevY
}
for x > 0 && y > 0 {
push(OpEqual)
x--
y--
}
// d == 0 leaves at most one of x, y non-zero only when the other
// sequence was fully consumed on the diagonal, which the prefix trim
// already handled; keep the guard rather than assume.
for ; x > 0; x-- {
push(OpDelete)
}
for ; y > 0; y-- {
push(OpInsert)
}
out := make([]edit, 0, len(rev))
for i := len(rev) - 1; i >= 0; i-- {
out = append(out, rev[i])
}
return coalesce(out)
}
func coalesce(in []edit) []edit {
out := in[:0:0]
for _, e := range in {
if e.n == 0 {
continue
}
if len(out) > 0 && out[len(out)-1].op == e.op {
out[len(out)-1].n += e.n
continue
}
out = append(out, e)
}
return out
}
// commonCount reports how many elements the edit script keeps equal.
func commonCount(script []edit) int {
n := 0
for _, e := range script {
if e.op == OpEqual {
n += e.n
}
}
return n
}
// ratio is the classic 2*common/(len(a)+len(b)) similarity in [0,1].
func ratio(a, b []int) float64 {
if len(a) == 0 && len(b) == 0 {
return 1
}
return 2 * float64(commonCount(diffInts(a, b))) / float64(len(a)+len(b))
}