From d8088cf74b3f90fb4c9435d2d906604a8877714f Mon Sep 17 00:00:00 2001 From: Han Kang Date: Tue, 11 Apr 2023 08:57:02 -0700 Subject: [PATCH 1/4] add generic sets package to utils --- sets/OWNERS | 10 ++ sets/set.go | 187 +++++++++++++++++++++++++++++++ sets/set_test.go | 286 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 483 insertions(+) create mode 100644 sets/OWNERS create mode 100644 sets/set.go create mode 100644 sets/set_test.go diff --git a/sets/OWNERS b/sets/OWNERS new file mode 100644 index 00000000..34bce769 --- /dev/null +++ b/sets/OWNERS @@ -0,0 +1,10 @@ +# See the OWNERS docs at https://go.k8s.io/owners + +reviewers: + - logicalhan + - thockin + - lavalamp +approvers: + - logicalhan + - thockin + - lavalamp \ No newline at end of file diff --git a/sets/set.go b/sets/set.go new file mode 100644 index 00000000..eb198da9 --- /dev/null +++ b/sets/set.go @@ -0,0 +1,187 @@ +/* +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 sets + +import "sort" + +type Ordered interface { + int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | uintptr | float32 | float64 | string +} + +// 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{} + +type Set[E Ordered] map[E]Empty + +// NewSet creates a new set. +func NewSet[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] { + 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 +} + +func (s Set[E]) Union(s2 Set[E]) Set[E] { + result := Set[E]{} + result.Insert(s.List()...) + result.Insert(s2.List()...) + return result +} + +// Len returns the size of the set. +func (s Set[E]) Len() int { + return len(s) +} + +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. +// (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) +} + +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] } + +// List returns the contents as a sorted int slice. +func (s Set[E]) List() []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(sortableSlice[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 +} diff --git a/sets/set_test.go b/sets/set_test.go new file mode 100644 index 00000000..e2ece316 --- /dev/null +++ b/sets/set_test.go @@ -0,0 +1,286 @@ +/* +Copyright 2014 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 sets + +import ( + "reflect" + "testing" +) + +func TestStringSet(t *testing.T) { + s := NewSet[string]() + s2 := NewSet[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 TestStringSetDeleteMultiples(t *testing.T) { + s := NewSet[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 TestNewStringSet(t *testing.T) { + s := NewSet[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 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 TestStringSetDifference(t *testing.T) { + a := NewSet[string]("1", "2", "3") + b := NewSet[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()) + } + 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()) + } +} + +func TestStringSetHasAny(t *testing.T) { + a := NewSet[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 := NewSet[string]("1", "2") + b := NewSet[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") + 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]() + if !a.Equal(b) { + t.Errorf("Expected to be equal: %v vs %v", a, b) + } + + b = NewSet[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", "") + 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.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] + }{ + { + NewSet[string]("1", "2", "3", "4"), + NewSet[string]("3", "4", "5", "6"), + NewSet[string]("1", "2", "3", "4", "5", "6"), + }, + { + NewSet[string]("1", "2", "3", "4"), + NewSet[string](), + NewSet[string]("1", "2", "3", "4"), + }, + { + NewSet[string](), + NewSet[string]("1", "2", "3", "4"), + NewSet[string]("1", "2", "3", "4"), + }, + { + NewSet[string](), + NewSet[string](), + NewSet[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.List(), test.expected.List()) + } + } +} + +func TestStringIntersection(t *testing.T) { + tests := []struct { + s1 Set[string] + s2 Set[string] + expected Set[string] + }{ + { + NewSet[string]("1", "2", "3", "4"), + NewSet[string]("3", "4", "5", "6"), + NewSet[string]("3", "4"), + }, + { + NewSet[string]("1", "2", "3", "4"), + NewSet[string]("1", "2", "3", "4"), + NewSet[string]("1", "2", "3", "4"), + }, + { + NewSet[string]("1", "2", "3", "4"), + NewSet[string](), + NewSet[string](), + }, + { + NewSet[string](), + NewSet[string]("1", "2", "3", "4"), + NewSet[string](), + }, + { + NewSet[string](), + NewSet[string](), + NewSet[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.List(), test.expected.List()) + } + } +} + +func TestNewSetFromMapKeys(t *testing.T) { + m := map[string]string{ + "hallo": "world", + "goodbye": "and goodnight", + } + expected := []string{"goodbye", "hallo"} + gotList := NewSetFromMapKeys(m).List() // 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() { + if entry != expected[i] { + t.Errorf("got %v, expected %v", entry, expected[i]) + } + } +} From 46762cfc4981d18a2751a641b8718c24c48b59b3 Mon Sep 17 00:00:00 2001 From: Han Kang Date: Fri, 5 May 2023 12:28:50 -0700 Subject: [PATCH 2/4] 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) +} From c3703b2ff0bf889af4f182c3e89fab3babac3053 Mon Sep 17 00:00:00 2001 From: Han Kang Date: Fri, 5 May 2023 12:33:15 -0700 Subject: [PATCH 3/4] fix lint issue --- set/OWNERS | 2 -- set/ordered.go | 2 +- set/set.go | 6 +++--- set/set_test.go | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/set/OWNERS b/set/OWNERS index 34bce769..9d2d33e7 100644 --- a/set/OWNERS +++ b/set/OWNERS @@ -3,8 +3,6 @@ reviewers: - logicalhan - thockin - - lavalamp approvers: - logicalhan - thockin - - lavalamp \ No newline at end of file diff --git a/set/ordered.go b/set/ordered.go index c7acaaec..6e5c6db3 100644 --- a/set/ordered.go +++ b/set/ordered.go @@ -1,5 +1,5 @@ /* -Copyright 2022 The Kubernetes Authors. +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. diff --git a/set/set.go b/set/set.go index 97d829b6..b115d0b7 100644 --- a/set/set.go +++ b/set/set.go @@ -1,5 +1,5 @@ /* -Copyright 2022 The Kubernetes Authors. +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. @@ -210,8 +210,8 @@ func (s Set[T]) Clone() Set[T] { // 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)) +func (s Set[T]) SymmetricDifference(s2 Set[T]) Set[T] { + return s.Difference(s2).Union(s2.Difference(s)) } // Clear empties the set. diff --git a/set/set_test.go b/set/set_test.go index 02f923a7..c910ce0a 100644 --- a/set/set_test.go +++ b/set/set_test.go @@ -1,5 +1,5 @@ /* -Copyright 2014 The Kubernetes Authors. +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. From 10cedcc11cf9b3497ebe1fbf7331a97e364589c4 Mon Sep 17 00:00:00 2001 From: Han Kang Date: Fri, 5 May 2023 13:11:43 -0700 Subject: [PATCH 4/4] address tim's comments --- set/ordered.go | 4 ++-- set/set.go | 12 ++++++------ set/set_test.go | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/set/ordered.go b/set/ordered.go index 6e5c6db3..2b2c11fc 100644 --- a/set/ordered.go +++ b/set/ordered.go @@ -16,11 +16,11 @@ limitations under the License. package set -// Ordered is a constraint that permits any ordered type: any type +// 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 { +type ordered interface { integer | float | ~string } diff --git a/set/set.go b/set/set.go index b115d0b7..172482cd 100644 --- a/set/set.go +++ b/set/set.go @@ -24,18 +24,18 @@ import ( // 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 +// 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] { +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] { +func KeySet[E ordered, A any](theMap map[E]A) Set[E] { ret := Set[E]{} for key := range theMap { ret.Insert(key) @@ -98,7 +98,7 @@ func (s Set[E]) Union(s2 Set[E]) Set[E] { return result } -// Len returns the size of the set. +// Len returns the number of elements in the set. func (s Set[E]) Len() int { return len(s) } @@ -158,7 +158,7 @@ func (s Set[E]) Equal(s2 Set[E]) bool { return s.Len() == s.Len() && s.IsSuperset(s2) } -type sortableSlice[E Ordered] []E +type sortableSlice[E ordered] []E func (s sortableSlice[E]) Len() int { return len(s) diff --git a/set/set_test.go b/set/set_test.go index c910ce0a..ea2d9d50 100644 --- a/set/set_test.go +++ b/set/set_test.go @@ -283,7 +283,7 @@ func TestStringIntersection(t *testing.T) { } } -func TestNewSetFromMapKeys(t *testing.T) { +func TestKeySet(t *testing.T) { m := map[string]string{ "hallo": "world", "goodbye": "and goodnight", @@ -359,7 +359,7 @@ func TestSetClearInSeparateFunction(t *testing.T) { } } -func clearSetAndAdd[T Ordered](s Set[T], a T) { +func clearSetAndAdd[T ordered](s Set[T], a T) { s.Clear() s.Insert(a) }