Skip to content

Commit f352f8a

Browse files
committed
util/set: move Slice type from corp to oss
This is an exact copy of the files misc/set/set{,_test}.go from tailscale/corp@a5415da, plus the license headers. For use in tailscale#7877 Signed-off-by: Andrew Dunham <andrew@du.nham.ca> Change-Id: I712d09c6d1a180c6633abe3acf8feb59b27e2866
1 parent 8dec1a8 commit f352f8a

File tree

2 files changed

+125
-0
lines changed

2 files changed

+125
-0
lines changed

util/set/slice.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
package set
5+
6+
import (
7+
"golang.org/x/exp/slices"
8+
"tailscale.com/types/views"
9+
)
10+
11+
// Slice is a set of elements tracked in a slice of unique elements.
12+
type Slice[T comparable] struct {
13+
slice []T
14+
set map[T]bool // nil until/unless slice is large enough
15+
}
16+
17+
// Slice returns the a view of the underlying slice.
18+
// The elements are in order of insertion.
19+
// The returned value is only valid until ss is modified again.
20+
func (ss *Slice[T]) Slice() views.Slice[T] { return views.SliceOf(ss.slice) }
21+
22+
// Contains reports whether v is in the set.
23+
// The amortized cost is O(1).
24+
func (ss *Slice[T]) Contains(v T) bool {
25+
if ss.set != nil {
26+
return ss.set[v]
27+
}
28+
return slices.Index(ss.slice, v) != -1
29+
}
30+
31+
// Remove removes v from the set.
32+
// The cost is O(n).
33+
func (ss *Slice[T]) Remove(v T) {
34+
if ss.set != nil {
35+
if !ss.set[v] {
36+
return
37+
}
38+
delete(ss.set, v)
39+
}
40+
if ix := slices.Index(ss.slice, v); ix != -1 {
41+
ss.slice = append(ss.slice[:ix], ss.slice[ix+1:]...)
42+
}
43+
}
44+
45+
// Add adds each element in vs to the set.
46+
// The amortized cost is O(1) per element.
47+
func (ss *Slice[T]) Add(vs ...T) {
48+
for _, v := range vs {
49+
if ss.Contains(v) {
50+
continue
51+
}
52+
ss.slice = append(ss.slice, v)
53+
if ss.set != nil {
54+
ss.set[v] = true
55+
} else if len(ss.slice) > 8 {
56+
ss.set = make(map[T]bool, len(ss.slice))
57+
for _, v := range ss.slice {
58+
ss.set[v] = true
59+
}
60+
}
61+
}
62+
}
63+
64+
// AddSlice adds all elements in vs to the set.
65+
func (ss *Slice[T]) AddSlice(vs views.Slice[T]) {
66+
for i := 0; i < vs.Len(); i++ {
67+
ss.Add(vs.At(i))
68+
}
69+
}

util/set/slice_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) Tailscale Inc & AUTHORS
2+
// SPDX-License-Identifier: BSD-3-Clause
3+
4+
package set
5+
6+
import (
7+
"testing"
8+
9+
qt "github.com/frankban/quicktest"
10+
)
11+
12+
func TestSliceSet(t *testing.T) {
13+
c := qt.New(t)
14+
15+
var ss Slice[int]
16+
c.Check(len(ss.slice), qt.Equals, 0)
17+
ss.Add(1)
18+
c.Check(len(ss.slice), qt.Equals, 1)
19+
c.Check(len(ss.set), qt.Equals, 0)
20+
c.Check(ss.Contains(1), qt.Equals, true)
21+
c.Check(ss.Contains(2), qt.Equals, false)
22+
23+
ss.Add(1)
24+
c.Check(len(ss.slice), qt.Equals, 1)
25+
c.Check(len(ss.set), qt.Equals, 0)
26+
27+
ss.Add(2)
28+
ss.Add(3)
29+
ss.Add(4)
30+
ss.Add(5)
31+
ss.Add(6)
32+
ss.Add(7)
33+
ss.Add(8)
34+
c.Check(len(ss.slice), qt.Equals, 8)
35+
c.Check(len(ss.set), qt.Equals, 0)
36+
37+
ss.Add(9)
38+
c.Check(len(ss.slice), qt.Equals, 9)
39+
c.Check(len(ss.set), qt.Equals, 9)
40+
41+
ss.Remove(4)
42+
c.Check(len(ss.slice), qt.Equals, 8)
43+
c.Check(len(ss.set), qt.Equals, 8)
44+
c.Assert(ss.Contains(4), qt.IsFalse)
45+
46+
// Ensure that the order of insertion is maintained
47+
c.Assert(ss.Slice().AsSlice(), qt.DeepEquals, []int{1, 2, 3, 5, 6, 7, 8, 9})
48+
ss.Add(4)
49+
c.Check(len(ss.slice), qt.Equals, 9)
50+
c.Check(len(ss.set), qt.Equals, 9)
51+
c.Assert(ss.Contains(4), qt.IsTrue)
52+
c.Assert(ss.Slice().AsSlice(), qt.DeepEquals, []int{1, 2, 3, 5, 6, 7, 8, 9, 4})
53+
54+
ss.Add(1, 234, 556)
55+
c.Assert(ss.Slice().AsSlice(), qt.DeepEquals, []int{1, 2, 3, 5, 6, 7, 8, 9, 4, 234, 556})
56+
}

0 commit comments

Comments
 (0)