diff --git a/set/OWNERS b/set/OWNERS new file mode 100644 index 00000000..9d2d33e7 --- /dev/null +++ b/set/OWNERS @@ -0,0 +1,8 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +reviewers: + - logicalhan + - thockin +approvers: + - logicalhan + - thockin diff --git a/set/ordered.go b/set/ordered.go new file mode 100644 index 00000000..2b2c11fc --- /dev/null +++ b/set/ordered.go @@ -0,0 +1,53 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package set + +// ordered is a constraint that permits any ordered type: any type +// that supports the operators < <= >= >. +// If future releases of Go add new ordered types, +// this constraint will be modified to include them. +type ordered interface { + integer | float | ~string +} + +// integer is a constraint that permits any integer type. +// If future releases of Go add new predeclared integer types, +// this constraint will be modified to include them. +type integer interface { + signed | unsigned +} + +// float is a constraint that permits any floating-point type. +// If future releases of Go add new predeclared floating-point types, +// this constraint will be modified to include them. +type float interface { + ~float32 | ~float64 +} + +// signed is a constraint that permits any signed integer type. +// If future releases of Go add new predeclared signed integer types, +// this constraint will be modified to include them. +type signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +// unsigned is a constraint that permits any unsigned integer type. +// If future releases of Go add new predeclared unsigned integer types, +// this constraint will be modified to include them. +type unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} diff --git a/set/set.go b/set/set.go new file mode 100644 index 00000000..172482cd --- /dev/null +++ b/set/set.go @@ -0,0 +1,229 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package set + +import ( + "sort" +) + +// Empty is public since it is used by some internal API objects for conversions between external +// string arrays and internal sets, and conversion logic requires public types today. +type Empty struct{} + +// Set is a set of the same type elements, implemented via map[ordered]struct{} for minimal memory consumption. +type Set[E ordered] map[E]Empty + +// New creates a new set. +func New[E ordered](items ...E) Set[E] { + ss := Set[E]{} + ss.Insert(items...) + return ss +} + +// KeySet creates a Set[E] from a keys of a map[E](? extends interface{}). +func KeySet[E ordered, A any](theMap map[E]A) Set[E] { + ret := Set[E]{} + for key := range theMap { + ret.Insert(key) + } + return ret +} + +// Insert adds items to the set. +func (s Set[E]) Insert(items ...E) Set[E] { + for _, item := range items { + s[item] = Empty{} + } + return s +} + +// Delete removes all items from the set. +func (s Set[E]) Delete(items ...E) Set[E] { + for _, item := range items { + delete(s, item) + } + return s +} + +// Has returns true if and only if item is contained in the set. +func (s Set[E]) Has(item E) bool { + _, contained := s[item] + return contained +} + +// HasAll returns true if and only if all items are contained in the set. +func (s Set[E]) HasAll(items ...E) bool { + for _, item := range items { + if !s.Has(item) { + return false + } + } + return true +} + +// HasAny returns true if any items are contained in the set. +func (s Set[E]) HasAny(items ...E) bool { + for _, item := range items { + if s.Has(item) { + return true + } + } + return false +} + +// Union returns a new set which includes items in either s1 or s2. +// For example: +// s1 = {a1, a2} +// s2 = {a3, a4} +// s1.Union(s2) = {a1, a2, a3, a4} +// s2.Union(s1) = {a1, a2, a3, a4} +func (s Set[E]) Union(s2 Set[E]) Set[E] { + result := Set[E]{} + result.Insert(s.UnsortedList()...) + result.Insert(s2.UnsortedList()...) + return result +} + +// Len returns the number of elements in the set. +func (s Set[E]) Len() int { + return len(s) +} + +// Intersection returns a new set which includes the item in BOTH s1 and s2 +// For example: +// s1 = {a1, a2} +// s2 = {a2, a3} +// s1.Intersection(s2) = {a2} +func (s Set[E]) Intersection(s2 Set[E]) Set[E] { + var walk, other Set[E] + result := Set[E]{} + if s.Len() < s2.Len() { + walk = s + other = s2 + } else { + walk = s2 + other = s + } + for key := range walk { + if other.Has(key) { + result.Insert(key) + } + } + return result +} + +// IsSuperset returns true if and only if s1 is a superset of s2. +func (s Set[E]) IsSuperset(s2 Set[E]) bool { + for item := range s2 { + if !s.Has(item) { + return false + } + } + return true +} + +// Difference returns a set of objects that are not in s2 +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.Difference(s2) = {a3} +// s2.Difference(s1) = {a4, a5} +func (s Set[E]) Difference(s2 Set[E]) Set[E] { + result := Set[E]{} + for key := range s { + if !s2.Has(key) { + result.Insert(key) + } + } + return result +} + +// Equal returns true if and only if s1 is equal (as a set) to s2. +// Two sets are equal if their membership is identical. +func (s Set[E]) Equal(s2 Set[E]) bool { + return s.Len() == s.Len() && s.IsSuperset(s2) +} + +type sortableSlice[E ordered] []E + +func (s sortableSlice[E]) Len() int { + return len(s) +} +func (s sortableSlice[E]) Less(i, j int) bool { return s[i] < s[j] } +func (s sortableSlice[E]) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// SortedList returns the contents as a sorted slice. +func (s Set[E]) SortedList() []E { + res := make(sortableSlice[E], 0, s.Len()) + for key := range s { + res = append(res, key) + } + sort.Sort(res) + return res +} + +// UnsortedList returns the slice with contents in random order. +func (s Set[E]) UnsortedList() []E { + res := make([]E, 0, len(s)) + for key := range s { + res = append(res, key) + } + return res +} + +// PopAny returns a single element from the set. +func (s Set[E]) PopAny() (E, bool) { + for key := range s { + s.Delete(key) + return key, true + } + var zeroValue E + return zeroValue, false +} + +// Clone returns a new set which is a copy of the current set. +func (s Set[T]) Clone() Set[T] { + result := make(Set[T], len(s)) + for key := range s { + result.Insert(key) + } + return result +} + +// SymmetricDifference returns a set of elements which are in either of the sets, but not in their intersection. +// For example: +// s1 = {a1, a2, a3} +// s2 = {a1, a2, a4, a5} +// s1.SymmetricDifference(s2) = {a3, a4, a5} +// s2.SymmetricDifference(s1) = {a3, a4, a5} +func (s Set[T]) SymmetricDifference(s2 Set[T]) Set[T] { + return s.Difference(s2).Union(s2.Difference(s)) +} + +// Clear empties the set. +// It is preferable to replace the set with a newly constructed set, +// but not all callers can do that (when there are other references to the map). +// In some cases the set *won't* be fully cleared, e.g. a Set[float32] containing NaN +// can't be cleared because NaN can't be removed. +// For sets containing items of a type that is reflexive for ==, +// this is optimized to a single call to runtime.mapclear(). +func (s Set[T]) Clear() Set[T] { + for key := range s { + delete(s, key) + } + return s +} diff --git a/set/set_test.go b/set/set_test.go new file mode 100644 index 00000000..ea2d9d50 --- /dev/null +++ b/set/set_test.go @@ -0,0 +1,365 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package set + +import ( + "reflect" + "testing" +) + +func TestStringSetHasAll(t *testing.T) { + s := New[string]() + s2 := New[string]() + if len(s) != 0 { + t.Errorf("Expected len=0: %d", len(s)) + } + s.Insert("a", "b") + if len(s) != 2 { + t.Errorf("Expected len=2: %d", len(s)) + } + s.Insert("c") + if s.Has("d") { + t.Errorf("Unexpected contents: %#v", s) + } + if !s.Has("a") { + t.Errorf("Missing contents: %#v", s) + } + s.Delete("a") + if s.Has("a") { + t.Errorf("Unexpected contents: %#v", s) + } + s.Insert("a") + if s.HasAll("a", "b", "d") { + t.Errorf("Unexpected contents: %#v", s) + } + if !s.HasAll("a", "b") { + t.Errorf("Missing contents: %#v", s) + } + s2.Insert("a", "b", "d") + if s.IsSuperset(s2) { + t.Errorf("Unexpected contents: %#v", s) + } + s2.Delete("d") + if !s.IsSuperset(s2) { + t.Errorf("Missing contents: %#v", s) + } +} + +func TestTypeInference(t *testing.T) { + s := New("a", "b", "c") + if len(s) != 3 { + t.Errorf("Expected len=3: %d", len(s)) + } +} + +func TestStringSetDeleteMultiples(t *testing.T) { + s := New[string]() + s.Insert("a", "b", "c") + if len(s) != 3 { + t.Errorf("Expected len=3: %d", len(s)) + } + + s.Delete("a", "c") + if len(s) != 1 { + t.Errorf("Expected len=1: %d", len(s)) + } + if s.Has("a") { + t.Errorf("Unexpected contents: %#v", s) + } + if s.Has("c") { + t.Errorf("Unexpected contents: %#v", s) + } + if !s.Has("b") { + t.Errorf("Missing contents: %#v", s) + } +} + +func TestNewStringSetWithMultipleStrings(t *testing.T) { + s := New[string]("a", "b", "c") + if len(s) != 3 { + t.Errorf("Expected len=3: %d", len(s)) + } + if !s.Has("a") || !s.Has("b") || !s.Has("c") { + t.Errorf("Unexpected contents: %#v", s) + } +} + +func TestStringSetSortedList(t *testing.T) { + s := New[string]("z", "y", "x", "a") + if !reflect.DeepEqual(s.SortedList(), []string{"a", "x", "y", "z"}) { + t.Errorf("SortedList gave unexpected result: %#v", s.SortedList()) + } +} + +func TestStringSetUnsortedList(t *testing.T) { + s := New[string]("z", "y", "x", "a") + ul := s.UnsortedList() + if len(ul) != 4 || !s.Has("z") || !s.Has("y") || !s.Has("x") || !s.Has("a") { + t.Errorf("UnsortedList gave unexpected result: %#v", s.UnsortedList()) + } +} + +func TestStringSetDifference(t *testing.T) { + a := New[string]("1", "2", "3") + b := New[string]("1", "2", "4", "5") + c := a.Difference(b) + d := b.Difference(a) + if len(c) != 1 { + t.Errorf("Expected len=1: %d", len(c)) + } + if !c.Has("3") { + t.Errorf("Unexpected contents: %#v", c.SortedList()) + } + if len(d) != 2 { + t.Errorf("Expected len=2: %d", len(d)) + } + if !d.Has("4") || !d.Has("5") { + t.Errorf("Unexpected contents: %#v", d.SortedList()) + } +} + +func TestStringSetHasAny(t *testing.T) { + a := New[string]("1", "2", "3") + + if !a.HasAny("1", "4") { + t.Errorf("expected true, got false") + } + + if a.HasAny("0", "4") { + t.Errorf("expected false, got true") + } +} + +func TestStringSetEquals(t *testing.T) { + // Simple case (order doesn't matter) + a := New[string]("1", "2") + b := New[string]("2", "1") + if !a.Equal(b) { + t.Errorf("Expected to be equal: %v vs %v", a, b) + } + + // It is a set; duplicates are ignored + b = New[string]("2", "2", "1") + if !a.Equal(b) { + t.Errorf("Expected to be equal: %v vs %v", a, b) + } + + // Edge cases around empty sets / empty strings + a = New[string]() + b = New[string]() + if !a.Equal(b) { + t.Errorf("Expected to be equal: %v vs %v", a, b) + } + + b = New[string]("1", "2", "3") + if a.Equal(b) { + t.Errorf("Expected to be not-equal: %v vs %v", a, b) + } + + b = New[string]("1", "2", "") + if a.Equal(b) { + t.Errorf("Expected to be not-equal: %v vs %v", a, b) + } + + // Check for equality after mutation + a = New[string]() + a.Insert("1") + if a.Equal(b) { + t.Errorf("Expected to be not-equal: %v vs %v", a, b) + } + + a.Insert("2") + if a.Equal(b) { + t.Errorf("Expected to be not-equal: %v vs %v", a, b) + } + + a.Insert("") + if !a.Equal(b) { + t.Errorf("Expected to be equal: %v vs %v", a, b) + } + + a.Delete("") + if a.Equal(b) { + t.Errorf("Expected to be not-equal: %v vs %v", a, b) + } +} + +func TestStringUnion(t *testing.T) { + tests := []struct { + s1 Set[string] + s2 Set[string] + expected Set[string] + }{ + { + New[string]("1", "2", "3", "4"), + New[string]("3", "4", "5", "6"), + New[string]("1", "2", "3", "4", "5", "6"), + }, + { + New[string]("1", "2", "3", "4"), + New[string](), + New[string]("1", "2", "3", "4"), + }, + { + New[string](), + New[string]("1", "2", "3", "4"), + New[string]("1", "2", "3", "4"), + }, + { + New[string](), + New[string](), + New[string](), + }, + } + + for _, test := range tests { + union := test.s1.Union(test.s2) + if union.Len() != test.expected.Len() { + t.Errorf("Expected union.Len()=%d but got %d", test.expected.Len(), union.Len()) + } + + if !union.Equal(test.expected) { + t.Errorf("Expected union.Equal(expected) but not true. union:%v expected:%v", union.SortedList(), test.expected.SortedList()) + } + } +} + +func TestStringIntersection(t *testing.T) { + tests := []struct { + s1 Set[string] + s2 Set[string] + expected Set[string] + }{ + { + New[string]("1", "2", "3", "4"), + New[string]("3", "4", "5", "6"), + New[string]("3", "4"), + }, + { + New[string]("1", "2", "3", "4"), + New[string]("1", "2", "3", "4"), + New[string]("1", "2", "3", "4"), + }, + { + New[string]("1", "2", "3", "4"), + New[string](), + New[string](), + }, + { + New[string](), + New[string]("1", "2", "3", "4"), + New[string](), + }, + { + New[string](), + New[string](), + New[string](), + }, + } + + for _, test := range tests { + intersection := test.s1.Intersection(test.s2) + if intersection.Len() != test.expected.Len() { + t.Errorf("Expected intersection.Len()=%d but got %d", test.expected.Len(), intersection.Len()) + } + + if !intersection.Equal(test.expected) { + t.Errorf("Expected intersection.Equal(expected) but not true. intersection:%v expected:%v", intersection.SortedList(), test.expected.SortedList()) + } + } +} + +func TestKeySet(t *testing.T) { + m := map[string]string{ + "hallo": "world", + "goodbye": "and goodnight", + } + expected := []string{"goodbye", "hallo"} + gotList := KeySet(m).SortedList() // List() returns a sorted list + if len(gotList) != len(m) { + t.Fatalf("got %v elements, wanted %v", len(gotList), len(m)) + } + for i, entry := range KeySet(m).SortedList() { + if entry != expected[i] { + t.Errorf("got %v, expected %v", entry, expected[i]) + } + } +} + +func TestSetSymmetricDifference(t *testing.T) { + a := New("1", "2", "3") + b := New("1", "2", "4", "5") + c := a.SymmetricDifference(b) + d := b.SymmetricDifference(a) + if !c.Equal(New("3", "4", "5")) { + t.Errorf("Unexpected contents: %#v", c.SortedList()) + } + if !d.Equal(New("3", "4", "5")) { + t.Errorf("Unexpected contents: %#v", d.SortedList()) + } +} + +func TestSetClear(t *testing.T) { + s := New[string]() + s.Insert("a", "b", "c") + if s.Len() != 3 { + t.Errorf("Expected len=3: %d", s.Len()) + } + + s.Clear() + if s.Len() != 0 { + t.Errorf("Expected len=0: %d", s.Len()) + } +} + +func TestSetClearWithSharedReference(t *testing.T) { + s := New[string]() + s.Insert("a", "b", "c") + if s.Len() != 3 { + t.Errorf("Expected len=3: %d", s.Len()) + } + + m := s + s.Clear() + if s.Len() != 0 { + t.Errorf("Expected len=0 on the cleared set: %d", s.Len()) + } + if m.Len() != 0 { + t.Errorf("Expected len=0 on the shared reference: %d", m.Len()) + } +} + +func TestSetClearInSeparateFunction(t *testing.T) { + s := New[string]() + s.Insert("a", "b", "c") + if s.Len() != 3 { + t.Errorf("Expected len=3: %d", s.Len()) + } + + clearSetAndAdd(s, "d") + if s.Len() != 1 { + t.Errorf("Expected len=1: %d", s.Len()) + } + if !s.Has("d") { + t.Errorf("Unexpected contents: %#v", s) + } +} + +func clearSetAndAdd[T ordered](s Set[T], a T) { + s.Clear() + s.Insert(a) +}