types/views: fix SliceEqualAnyOrderFunc short optimization

This was flagged by @tkhattra on the merge commit; thanks!

Updates tailscale/corp#25479

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ia8045640f02bd4dcc0fe7433249fd72ac6b9cf52
This commit is contained in:
Andrew Dunham 2025-01-24 13:26:08 -05:00
parent 0aa54151f2
commit eb299302ba
2 changed files with 64 additions and 6 deletions

View File

@ -386,14 +386,32 @@ func SliceEqualAnyOrderFunc[T any, V comparable](a, b Slice[T], cmp func(T) V) b
// do the quadratic thing. We can also only check the items between // do the quadratic thing. We can also only check the items between
// diffStart and the end. // diffStart and the end.
nRemain := a.Len() - diffStart nRemain := a.Len() - diffStart
if nRemain <= 5 { const shortOptLen = 5
maxLen := a.Len() // same as b.Len() if nRemain <= shortOptLen {
for i := diffStart; i < maxLen; i++ { // These track which elements in a and b have been matched, so
av := cmp(a.At(i)) // that we don't treat arrays with differing number of
// duplicate elements as equal (e.g. [1, 1, 2] and [1, 2, 2]).
var aMatched, bMatched [shortOptLen]bool
// Compare each element in a to each element in b
for i := range nRemain {
av := cmp(a.At(i + diffStart))
found := false found := false
for j := diffStart; j < maxLen; j++ { for j := range nRemain {
bv := cmp(b.At(j)) // Skip elements in b that have already been
// used to match an item in a.
if bMatched[j] {
continue
}
bv := cmp(b.At(j + diffStart))
if av == bv { if av == bv {
// Mark these elements as already
// matched, so that a future loop
// iteration (of a duplicate element)
// doesn't match it again.
aMatched[i] = true
bMatched[j] = true
found = true found = true
break break
} }
@ -402,6 +420,14 @@ func SliceEqualAnyOrderFunc[T any, V comparable](a, b Slice[T], cmp func(T) V) b
return false return false
} }
} }
// Verify all elements were matched exactly once.
for i := range nRemain {
if !aMatched[i] || !bMatched[i] {
return false
}
}
return true return true
} }

View File

@ -197,6 +197,38 @@ func TestSliceEqualAnyOrderFunc(t *testing.T) {
// Long difference; past the quadratic limit // Long difference; past the quadratic limit
longDiff := ncFrom("b", "a", "c", "d", "e", "f", "g", "h", "i", "k") // differs at end longDiff := ncFrom("b", "a", "c", "d", "e", "f", "g", "h", "i", "k") // differs at end
c.Check(SliceEqualAnyOrderFunc(longSlice, longDiff, cmp), qt.Equals, false) c.Check(SliceEqualAnyOrderFunc(longSlice, longDiff, cmp), qt.Equals, false)
// The short slice optimization had a bug where it wouldn't handle
// duplicate elements; test various cases here driven by code coverage.
shortTestCases := []struct {
name string
s1, s2 Slice[nc]
want bool
}{
{
name: "duplicates_same_length",
s1: ncFrom("a", "a", "b"),
s2: ncFrom("a", "b", "b"),
want: false,
},
{
name: "duplicates_different_matched",
s1: ncFrom("x", "y", "a", "a", "b"),
s2: ncFrom("x", "y", "b", "a", "a"),
want: true,
},
{
name: "item_in_a_not_b",
s1: ncFrom("x", "y", "a", "b", "c"),
s2: ncFrom("x", "y", "b", "c", "q"),
want: false,
},
}
for _, tc := range shortTestCases {
t.Run("short_"+tc.name, func(t *testing.T) {
c.Check(SliceEqualAnyOrderFunc(tc.s1, tc.s2, cmp), qt.Equals, tc.want)
})
}
} }
func TestSliceEqual(t *testing.T) { func TestSliceEqual(t *testing.T) {