~bigbes/sr-ht-spec

ref: 54fac8ddcf7a3232e2335300db465a6244a1b4be sr-ht-spec/prosediff/myers_test.go -rw-r--r-- 3.3 KiB
54fac8dd — bigbes docs: resolve bilingual search with per-line routing 27 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package prosediff

import (
	"math/rand"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestDiffIntsScripts(t *testing.T) {
	tests := []struct {
		name string
		a, b []int
		want []edit
	}{
		{"both empty", nil, nil, nil},
		{"identical", []int{1, 2, 3}, []int{1, 2, 3}, []edit{{OpEqual, 3}}},
		{"all inserted", nil, []int{1, 2}, []edit{{OpInsert, 2}}},
		{"all deleted", []int{1, 2}, nil, []edit{{OpDelete, 2}}},
		{"middle replaced", []int{1, 2, 3}, []int{1, 9, 3},
			[]edit{{OpEqual, 1}, {OpDelete, 1}, {OpInsert, 1}, {OpEqual, 1}}},
		{"suffix appended", []int{1, 2}, []int{1, 2, 3}, []edit{{OpEqual, 2}, {OpInsert, 1}}},
		{"prefix removed", []int{0, 1, 2}, []int{1, 2}, []edit{{OpDelete, 1}, {OpEqual, 2}}},
		{"nothing in common", []int{1, 2}, []int{3, 4},
			[]edit{{OpDelete, 2}, {OpInsert, 2}}},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			got := diffInts(tc.a, tc.b)
			assert.Equal(t, tc.want, got)
			assertScriptApplies(t, tc.a, tc.b, got)
		})
	}
}

// TestDiffIntsMinimalRandom is the real correctness check: on random inputs
// the script must both reconstruct b from a and keep exactly as many elements
// equal as the longest common subsequence.
func TestDiffIntsMinimalRandom(t *testing.T) {
	rng := rand.New(rand.NewSource(1))
	for iter := 0; iter < 400; iter++ {
		a := randSeq(rng, rng.Intn(14), 5)
		b := randSeq(rng, rng.Intn(14), 5)
		script := diffInts(a, b)
		assertScriptApplies(t, a, b, script)
		require.Equal(t, lcsLen(a, b), commonCount(script),
			"non-minimal script for a=%v b=%v: %v", a, b, script)
		for i := 1; i < len(script); i++ {
			require.NotEqual(t, script[i-1].op, script[i].op, "runs not coalesced: %v", script)
		}
	}
}

func TestRatio(t *testing.T) {
	assert.Equal(t, 1.0, ratio(nil, nil))
	assert.Equal(t, 1.0, ratio([]int{1, 2}, []int{1, 2}))
	assert.Equal(t, 0.0, ratio([]int{1, 2}, []int{3, 4}))
	assert.Equal(t, 0.5, ratio([]int{1, 2}, []int{1, 3}))
}

func randSeq(rng *rand.Rand, n, alphabet int) []int {
	if n == 0 {
		return nil
	}
	out := make([]int, n)
	for i := range out {
		out[i] = rng.Intn(alphabet)
	}
	return out
}

func assertScriptApplies(t *testing.T, a, b []int, script []edit) {
	t.Helper()
	var gotA, gotB []int
	i, j := 0, 0
	for _, e := range script {
		switch e.op {
		case OpEqual:
			require.LessOrEqual(t, i+e.n, len(a))
			require.LessOrEqual(t, j+e.n, len(b))
			for n := 0; n < e.n; n++ {
				require.Equal(t, a[i+n], b[j+n], "equal run over differing elements")
			}
			gotA = append(gotA, a[i:i+e.n]...)
			gotB = append(gotB, b[j:j+e.n]...)
			i += e.n
			j += e.n
		case OpDelete:
			gotA = append(gotA, a[i:i+e.n]...)
			i += e.n
		case OpInsert:
			gotB = append(gotB, b[j:j+e.n]...)
			j += e.n
		}
	}
	require.Equal(t, len(a), i)
	require.Equal(t, len(b), j)
	require.Equal(t, a, sliceOrNil(gotA))
	require.Equal(t, b, sliceOrNil(gotB))
}

func sliceOrNil(s []int) []int {
	if len(s) == 0 {
		return nil
	}
	return s
}

// lcsLen is the textbook O(nm) LCS, used only as the oracle.
func lcsLen(a, b []int) int {
	dp := make([][]int, len(a)+1)
	for i := range dp {
		dp[i] = make([]int, len(b)+1)
	}
	for i := 1; i <= len(a); i++ {
		for j := 1; j <= len(b); j++ {
			if a[i-1] == b[j-1] {
				dp[i][j] = dp[i-1][j-1] + 1
				continue
			}
			dp[i][j] = max(dp[i-1][j], dp[i][j-1])
		}
	}
	return dp[len(a)][len(b)]
}