From 46762cfc4981d18a2751a641b8718c24c48b59b3 Mon Sep 17 00:00:00 2001 From: Han Kang Date: Fri, 5 May 2023 12:28:50 -0700 Subject: [PATCH] make this implementation of sets compatible with the one in apimachinery --- {sets => set}/OWNERS | 0 set/ordered.go | 53 +++++++++++ {sets => set}/set.go | 74 +++++++++++---- {sets => set}/set_test.go | 189 +++++++++++++++++++++++++++----------- 4 files changed, 245 insertions(+), 71 deletions(-) rename {sets => set}/OWNERS (100%) create mode 100644 set/ordered.go rename {sets => set}/set.go (65%) rename {sets => set}/set_test.go (56%) diff --git a/sets/OWNERS b/set/OWNERS similarity index 100% rename from sets/OWNERS rename to set/OWNERS diff --git a/set/ordered.go b/set/ordered.go new file mode 100644 index 00000000..c7acaaec --- /dev/null +++ b/set/ordered.go @@ -0,0 +1,53 @@ +/* +Copyright 2022 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/sets/set.go b/set/set.go similarity index 65% rename from sets/set.go rename to set/set.go index eb198da9..97d829b6 100644 --- a/sets/set.go +++ b/set/set.go @@ -14,29 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. */ -package sets +package set -import "sort" - -type Ordered interface { - int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr | float32 | float64 | string -} +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[comparable]struct{} for minimal memory consumption. type Set[E Ordered] map[E]Empty -// NewSet creates a new set. -func NewSet[E Ordered](items ...E) Set[E] { +// New creates a new set. +func New[E Ordered](items ...E) Set[E] { ss := Set[E]{} ss.Insert(items...) return ss } -// NewSetFromMapKeys creates a Set[E] from a keys of a map[E](? extends interface{}). -func NewSetFromMapKeys[E Ordered, A any](theMap map[E]A) Set[E] { +// 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) @@ -86,10 +85,16 @@ func (s Set[E]) HasAny(items ...E) bool { 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.List()...) - result.Insert(s2.List()...) + result.Insert(s.UnsortedList()...) + result.Insert(s2.UnsortedList()...) return result } @@ -98,6 +103,11 @@ 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]{} @@ -144,7 +154,6 @@ func (s Set[E]) Difference(s2 Set[E]) Set[E] { // Equal returns true if and only if s1 is equal (as a set) to s2. // Two sets are equal if their membership is identical. -// (In practice, this means same elements, order doesn't matter) func (s Set[E]) Equal(s2 Set[E]) bool { return s.Len() == s.Len() && s.IsSuperset(s2) } @@ -157,8 +166,8 @@ func (s sortableSlice[E]) Len() int { 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] } -// List returns the contents as a sorted int slice. -func (s Set[E]) List() []E { +// 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) @@ -169,7 +178,7 @@ func (s Set[E]) List() []E { // UnsortedList returns the slice with contents in random order. func (s Set[E]) UnsortedList() []E { - res := make(sortableSlice[E], 0, len(s)) + res := make([]E, 0, len(s)) for key := range s { res = append(res, key) } @@ -185,3 +194,36 @@ func (s Set[E]) PopAny() (E, bool) { 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 (s1 Set[T]) SymmetricDifference(s2 Set[T]) Set[T] { + return s1.Difference(s2).Union(s2.Difference(s1)) +} + +// 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/sets/set_test.go b/set/set_test.go similarity index 56% rename from sets/set_test.go rename to set/set_test.go index e2ece316..02f923a7 100644 --- a/sets/set_test.go +++ b/set/set_test.go @@ -14,16 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -package sets +package set import ( "reflect" "testing" ) -func TestStringSet(t *testing.T) { - s := NewSet[string]() - s2 := NewSet[string]() +func TestStringSetHasAll(t *testing.T) { + s := New[string]() + s2 := New[string]() if len(s) != 0 { t.Errorf("Expected len=0: %d", len(s)) } @@ -59,8 +59,15 @@ func TestStringSet(t *testing.T) { } } +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 := NewSet[string]() + s := New[string]() s.Insert("a", "b", "c") if len(s) != 3 { t.Errorf("Expected len=3: %d", len(s)) @@ -81,8 +88,8 @@ func TestStringSetDeleteMultiples(t *testing.T) { } } -func TestNewStringSet(t *testing.T) { - s := NewSet[string]("a", "b", "c") +func TestNewStringSetWithMultipleStrings(t *testing.T) { + s := New[string]("a", "b", "c") if len(s) != 3 { t.Errorf("Expected len=3: %d", len(s)) } @@ -91,34 +98,42 @@ func TestNewStringSet(t *testing.T) { } } -func TestStringSetList(t *testing.T) { - s := NewSet[string]("z", "y", "x", "a") - if !reflect.DeepEqual(s.List(), []string{"a", "x", "y", "z"}) { - t.Errorf("List gave unexpected result: %#v", s.List()) +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 := NewSet[string]("1", "2", "3") - b := NewSet[string]("1", "2", "4", "5") + 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.List()) + 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.List()) + t.Errorf("Unexpected contents: %#v", d.SortedList()) } } func TestStringSetHasAny(t *testing.T) { - a := NewSet[string]("1", "2", "3") + a := New[string]("1", "2", "3") if !a.HasAny("1", "4") { t.Errorf("expected true, got false") @@ -131,37 +146,37 @@ func TestStringSetHasAny(t *testing.T) { func TestStringSetEquals(t *testing.T) { // Simple case (order doesn't matter) - a := NewSet[string]("1", "2") - b := NewSet[string]("2", "1") + 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 = NewSet[string]("2", "2", "1") + 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 = NewSet[string]() - b = NewSet[string]() + a = New[string]() + b = New[string]() if !a.Equal(b) { t.Errorf("Expected to be equal: %v vs %v", a, b) } - b = NewSet[string]("1", "2", "3") + b = New[string]("1", "2", "3") if a.Equal(b) { t.Errorf("Expected to be not-equal: %v vs %v", a, b) } - b = NewSet[string]("1", "2", "") + 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 = NewSet[string]() + a = New[string]() a.Insert("1") if a.Equal(b) { t.Errorf("Expected to be not-equal: %v vs %v", a, b) @@ -190,24 +205,24 @@ func TestStringUnion(t *testing.T) { expected Set[string] }{ { - NewSet[string]("1", "2", "3", "4"), - NewSet[string]("3", "4", "5", "6"), - NewSet[string]("1", "2", "3", "4", "5", "6"), + New[string]("1", "2", "3", "4"), + New[string]("3", "4", "5", "6"), + New[string]("1", "2", "3", "4", "5", "6"), }, { - NewSet[string]("1", "2", "3", "4"), - NewSet[string](), - NewSet[string]("1", "2", "3", "4"), + New[string]("1", "2", "3", "4"), + New[string](), + New[string]("1", "2", "3", "4"), }, { - NewSet[string](), - NewSet[string]("1", "2", "3", "4"), - NewSet[string]("1", "2", "3", "4"), + New[string](), + New[string]("1", "2", "3", "4"), + New[string]("1", "2", "3", "4"), }, { - NewSet[string](), - NewSet[string](), - NewSet[string](), + New[string](), + New[string](), + New[string](), }, } @@ -218,7 +233,7 @@ func TestStringUnion(t *testing.T) { } if !union.Equal(test.expected) { - t.Errorf("Expected union.Equal(expected) but not true. union:%v expected:%v", union.List(), test.expected.List()) + t.Errorf("Expected union.Equal(expected) but not true. union:%v expected:%v", union.SortedList(), test.expected.SortedList()) } } } @@ -230,29 +245,29 @@ func TestStringIntersection(t *testing.T) { expected Set[string] }{ { - NewSet[string]("1", "2", "3", "4"), - NewSet[string]("3", "4", "5", "6"), - NewSet[string]("3", "4"), + New[string]("1", "2", "3", "4"), + New[string]("3", "4", "5", "6"), + New[string]("3", "4"), }, { - NewSet[string]("1", "2", "3", "4"), - NewSet[string]("1", "2", "3", "4"), - NewSet[string]("1", "2", "3", "4"), + New[string]("1", "2", "3", "4"), + New[string]("1", "2", "3", "4"), + New[string]("1", "2", "3", "4"), }, { - NewSet[string]("1", "2", "3", "4"), - NewSet[string](), - NewSet[string](), + New[string]("1", "2", "3", "4"), + New[string](), + New[string](), }, { - NewSet[string](), - NewSet[string]("1", "2", "3", "4"), - NewSet[string](), + New[string](), + New[string]("1", "2", "3", "4"), + New[string](), }, { - NewSet[string](), - NewSet[string](), - NewSet[string](), + New[string](), + New[string](), + New[string](), }, } @@ -263,7 +278,7 @@ func TestStringIntersection(t *testing.T) { } if !intersection.Equal(test.expected) { - t.Errorf("Expected intersection.Equal(expected) but not true. intersection:%v expected:%v", intersection.List(), test.expected.List()) + t.Errorf("Expected intersection.Equal(expected) but not true. intersection:%v expected:%v", intersection.SortedList(), test.expected.SortedList()) } } } @@ -274,13 +289,77 @@ func TestNewSetFromMapKeys(t *testing.T) { "goodbye": "and goodnight", } expected := []string{"goodbye", "hallo"} - gotList := NewSetFromMapKeys(m).List() // List() returns a sorted list + 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 NewSetFromMapKeys(m).List() { + 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) +}