plans/objchange: LongestCommonSubsequence
This algorithm is the usual first step when generating diffs. This package is a bit of a strange home for it, but since it works with changes to cty.Value this feels more natural than any other place it could be.
This commit is contained in:
parent
1ced176fc6
commit
1aa9ac14cc
|
@ -0,0 +1,104 @@
|
|||
package objchange
|
||||
|
||||
import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// LongestCommonSubsequence finds a sequence of values that are common to both
|
||||
// x and y, with the same relative ordering as in both collections. This result
|
||||
// is useful as a first step towards computing a diff showing added/removed
|
||||
// elements in a sequence.
|
||||
//
|
||||
// The approached used here is a "naive" one, assuming that both xs and ys will
|
||||
// generally be small in most reasonable Terraform configurations. For larger
|
||||
// lists the time/space usage may be sub-optimal.
|
||||
//
|
||||
// A pair of lists may have multiple longest common subsequences. In that
|
||||
// case, the one selected by this function is undefined.
|
||||
func LongestCommonSubsequence(xs, ys []cty.Value) []cty.Value {
|
||||
if len(xs) == 0 || len(ys) == 0 {
|
||||
return make([]cty.Value, 0)
|
||||
}
|
||||
|
||||
c := make([]int, len(xs)*len(ys))
|
||||
eqs := make([]bool, len(xs)*len(ys))
|
||||
w := len(xs)
|
||||
|
||||
for y := 0; y < len(ys); y++ {
|
||||
for x := 0; x < len(xs); x++ {
|
||||
eqV := xs[x].Equals(ys[y])
|
||||
eq := false
|
||||
if eqV.IsKnown() && eqV.True() {
|
||||
eq = true
|
||||
eqs[(w*y)+x] = true // equality tests can be expensive, so cache it
|
||||
}
|
||||
if eq {
|
||||
// Sequence gets one longer than for the cell at top left,
|
||||
// since we'd append a new item to the sequence here.
|
||||
if x == 0 || y == 0 {
|
||||
c[(w*y)+x] = 1
|
||||
} else {
|
||||
c[(w*y)+x] = c[(w*(y-1))+(x-1)] + 1
|
||||
}
|
||||
} else {
|
||||
// We follow the longest of the sequence above and the sequence
|
||||
// to the left of us in the matrix.
|
||||
l := 0
|
||||
u := 0
|
||||
if x > 0 {
|
||||
l = c[(w*y)+(x-1)]
|
||||
}
|
||||
if y > 0 {
|
||||
u = c[(w*(y-1))+x]
|
||||
}
|
||||
if l > u {
|
||||
c[(w*y)+x] = l
|
||||
} else {
|
||||
c[(w*y)+x] = u
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The bottom right cell tells us how long our longest sequence will be
|
||||
seq := make([]cty.Value, c[len(c)-1])
|
||||
|
||||
// Now we will walk back from the bottom right cell, finding again all
|
||||
// of the equal pairs to construct our sequence.
|
||||
x := len(xs) - 1
|
||||
y := len(ys) - 1
|
||||
i := len(seq) - 1
|
||||
|
||||
for x > -1 && y > -1 {
|
||||
if eqs[(w*y)+x] {
|
||||
// Add the value to our result list and then walk diagonally
|
||||
// up and to the left.
|
||||
seq[i] = xs[x]
|
||||
x--
|
||||
y--
|
||||
i--
|
||||
} else {
|
||||
// Take the path with the greatest sequence length in the matrix.
|
||||
l := 0
|
||||
u := 0
|
||||
if x > 0 {
|
||||
l = c[(w*y)+(x-1)]
|
||||
}
|
||||
if y > 0 {
|
||||
u = c[(w*(y-1))+x]
|
||||
}
|
||||
if l > u {
|
||||
x--
|
||||
} else {
|
||||
y--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if i > -1 {
|
||||
// should never happen if the matrix was constructed properly
|
||||
panic("not enough elements in sequence")
|
||||
}
|
||||
|
||||
return seq
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
package objchange
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
func TestLongestCommonSubsequence(t *testing.T) {
|
||||
tests := []struct {
|
||||
xs []cty.Value
|
||||
ys []cty.Value
|
||||
want []cty.Value
|
||||
}{
|
||||
{
|
||||
[]cty.Value{},
|
||||
[]cty.Value{},
|
||||
[]cty.Value{},
|
||||
},
|
||||
{
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2)},
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2)},
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2)},
|
||||
},
|
||||
{
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2)},
|
||||
[]cty.Value{cty.NumberIntVal(3), cty.NumberIntVal(4)},
|
||||
[]cty.Value{},
|
||||
},
|
||||
{
|
||||
[]cty.Value{cty.NumberIntVal(2)},
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2)},
|
||||
[]cty.Value{cty.NumberIntVal(2)},
|
||||
},
|
||||
{
|
||||
[]cty.Value{cty.NumberIntVal(1)},
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2)},
|
||||
[]cty.Value{cty.NumberIntVal(1)},
|
||||
},
|
||||
{
|
||||
[]cty.Value{cty.NumberIntVal(2), cty.NumberIntVal(1)},
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2)},
|
||||
[]cty.Value{cty.NumberIntVal(1)}, // arbitrarily selected 1; 2 would also be valid
|
||||
},
|
||||
{
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2), cty.NumberIntVal(3), cty.NumberIntVal(4)},
|
||||
[]cty.Value{cty.NumberIntVal(2), cty.NumberIntVal(4), cty.NumberIntVal(5)},
|
||||
[]cty.Value{cty.NumberIntVal(2), cty.NumberIntVal(4)},
|
||||
},
|
||||
{
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2), cty.NumberIntVal(3), cty.NumberIntVal(4)},
|
||||
[]cty.Value{cty.NumberIntVal(4), cty.NumberIntVal(2), cty.NumberIntVal(5)},
|
||||
[]cty.Value{cty.NumberIntVal(4)}, // 2 would also be valid
|
||||
},
|
||||
{
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2), cty.NumberIntVal(3), cty.NumberIntVal(5)},
|
||||
[]cty.Value{cty.NumberIntVal(2), cty.NumberIntVal(4), cty.NumberIntVal(5)},
|
||||
[]cty.Value{cty.NumberIntVal(2), cty.NumberIntVal(5)},
|
||||
},
|
||||
|
||||
// unknowns never compare as equal
|
||||
{
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.UnknownVal(cty.Number), cty.NumberIntVal(3)},
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.UnknownVal(cty.Number), cty.NumberIntVal(3)},
|
||||
[]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(3)},
|
||||
},
|
||||
{
|
||||
[]cty.Value{cty.UnknownVal(cty.Number)},
|
||||
[]cty.Value{cty.UnknownVal(cty.Number)},
|
||||
[]cty.Value{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%#v,%#v", test.xs, test.ys), func(t *testing.T) {
|
||||
got := LongestCommonSubsequence(test.xs, test.ys)
|
||||
|
||||
wrong := func() {
|
||||
t.Fatalf(
|
||||
"wrong result\nX: %#v\nY: %#v\ngot: %#v\nwant: %#v",
|
||||
test.xs, test.ys, got, test.want,
|
||||
)
|
||||
}
|
||||
|
||||
if len(got) != len(test.want) {
|
||||
wrong()
|
||||
}
|
||||
|
||||
for i := range got {
|
||||
if got[i] == cty.NilVal {
|
||||
wrong()
|
||||
}
|
||||
if !got[i].RawEquals(test.want[i]) {
|
||||
wrong()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue