adding set interface and threadsafe implementation

This commit is contained in:
William Dillon 2026-07-24 23:29:07 -04:00
parent 4ea82a9ed2
commit 5ae74c0ea0
9 changed files with 104858 additions and 258 deletions

2
go.mod
View File

@ -2,4 +2,4 @@ module code.wmdillon.com/wmdillon/set
go 1.25.3 go 1.25.3
require golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 require golang.org/x/exp v0.0.0-20260718201538-764159d718ef

2
go.sum
View File

@ -1,2 +1,4 @@
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0= golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
golang.org/x/exp v0.0.0-20260718201538-764159d718ef h1:LkZ48HFgy/TvhTI0bcWkjgFkgLyKUwcTbDjS0DUjw+A=
golang.org/x/exp v0.0.0-20260718201538-764159d718ef/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q=

80
set.go
View File

@ -1,75 +1,23 @@
package set package set
import ( import (
"maps" "code.wmdillon.com/wmdillon/set/simpleset"
"code.wmdillon.com/wmdillon/set/threadsafeset"
) )
type Set[T comparable] map[T]struct{} type Set[T comparable] interface {
Add(element T)
func New[T comparable](elements ...T) Set[T] { Remove(element T)
s := Set[T]{} Contains(element T) bool
for _, e := range elements { Size() int
s.Add(e) ToSlice() []T
}
return s
} }
func (s Set[T]) Equal(other Set[T]) bool { func New[T comparable](threadsafe bool, elements ...T) Set[T] {
return maps.Equal(s, other) switch threadsafe {
} case true:
return threadsafeset.New(elements...)
func (s Set[T]) Add(element T) { default:
s[element] = struct{}{} return simpleset.New(elements...)
}
func (s Set[T]) Remove(element T) {
delete(s, element)
}
func (s Set[T]) Contains(element T) bool {
_, exists := s[element]
return exists
}
func (s Set[T]) Size() int {
return len(s)
}
func (s Set[T]) ToSlice() []T {
elements := make([]T, 0, len(s))
for e := range s {
elements = append(elements, e)
}
return elements
}
func (s Set[T]) Union(other Set[T]) Set[T] {
result := New[T]()
for e := range s {
result.Add(e)
}
for e := range other {
result.Add(e)
}
return result
}
func (s Set[T]) Intersection(other Set[T]) Set[T] {
result := New[T]()
for e := range s {
if other.Contains(e) {
result.Add(e)
} }
} }
return result
}
func (s Set[T]) Difference(other Set[T]) Set[T] {
result := New[T]()
for e := range s {
if !other.Contains(e) {
result.Add(e)
}
}
return result
}

View File

@ -1,196 +1,21 @@
package set package set
import "testing" import (
"testing"
func TestIntersection(t *testing.T) { "code.wmdillon.com/wmdillon/set/simpleset"
s1 := New[int](1, 2, 3, 4) "code.wmdillon.com/wmdillon/set/threadsafeset"
s2 := New[int](3, 4, 5, 6) )
want := New[int](3, 4)
got := s1.Intersection(s2)
if !got.Equal(want) {
t.Errorf("Intersection() = %v; want %v", got, want)
}
}
func TestIntersection_Empty(t *testing.T) { func TestNew(t *testing.T) {
s1 := New[int](1, 2) wantThreadsafe := false
s2 := New[int](3, 4) set := New[string](wantThreadsafe)
want := New[int]() if _, ok := set.(*simpleset.Set[string]); !ok {
got := s1.Intersection(s2) t.Fatalf("error: wanted *simpleset.Set[string]; got %T\n", set)
if !got.Equal(want) { }
t.Errorf("Intersection() = %v; want %v", got, want) wantThreadsafe = true
} set = New[string](wantThreadsafe)
} if _, ok := set.(*threadsafeset.Set[string]); !ok {
t.Fatalf("error: wanted *threadsafeset.Set[string]; got %T\n", set)
func TestIntersection_Identical(t *testing.T) {
s1 := New[int](1, 2, 3)
s2 := New[int](1, 2, 3)
want := New[int](1, 2, 3)
got := s1.Intersection(s2)
if !got.Equal(want) {
t.Errorf("Intersection() = %v; want %v", got, want)
}
}
func TestIntersection_Subset(t *testing.T) {
s1 := New[int](1, 2, 3, 4, 5)
s2 := New[int](2, 3)
want := New[int](2, 3)
got := s1.Intersection(s2)
if !got.Equal(want) {
t.Errorf("Intersection() = %v; want %v", got, want)
}
}
func TestIntersection_EmptySet(t *testing.T) {
s1 := New[int](1, 2, 3)
s2 := New[int]()
want := New[int]()
got := s1.Intersection(s2)
if !got.Equal(want) {
t.Errorf("Intersection() = %v; want %v", got, want)
}
}
func TestIntersection_BothEmpty(t *testing.T) {
s1 := New[int]()
s2 := New[int]()
want := New[int]()
got := s1.Intersection(s2)
if !got.Equal(want) {
t.Errorf("Intersection() = %v; want %v", got, want)
}
}
func TestDifference(t *testing.T) {
s1 := New[int](1, 2, 3, 4)
s2 := New[int](3, 4, 5, 6)
want := New[int](1, 2)
got := s1.Difference(s2)
if !got.Equal(want) {
t.Errorf("Difference() = %v; want %v", got, want)
}
}
func TestDifference_Empty(t *testing.T) {
s1 := New[int](1, 2)
s2 := New[int](1, 2)
want := New[int]()
got := s1.Difference(s2)
if !got.Equal(want) {
t.Errorf("Difference() = %v; want %v", got, want)
}
}
func TestDifference_Subset(t *testing.T) {
s1 := New[int](1, 2, 3, 4, 5)
s2 := New[int](2, 3)
want := New[int](1, 4, 5)
got := s1.Difference(s2)
if !got.Equal(want) {
t.Errorf("Difference() = %v; want %v", got, want)
}
}
func TestDifference_EmptySet(t *testing.T) {
s1 := New[int](1, 2, 3)
s2 := New[int]()
want := New[int](1, 2, 3)
got := s1.Difference(s2)
if !got.Equal(want) {
t.Errorf("Difference() = %v; want %v", got, want)
}
}
func TestDifference_BothEmpty(t *testing.T) {
s1 := New[int]()
s2 := New[int]()
want := New[int]()
got := s1.Difference(s2)
if !got.Equal(want) {
t.Errorf("Difference() = %v; want %v", got, want)
}
}
func TestUnion(t *testing.T) {
s1 := New[int](1, 2, 3)
s2 := New[int](3, 4, 5)
want := New[int](1, 2, 3, 4, 5)
got := s1.Union(s2)
if !got.Equal(want) {
t.Errorf("Union() = %v; want %v", got, want)
}
}
func TestUnion_EmptySet(t *testing.T) {
s1 := New[int](1, 2, 3)
s2 := New[int]()
want := New[int](1, 2, 3)
got := s1.Union(s2)
if !got.Equal(want) {
t.Errorf("Union() = %v; want %v", got, want)
}
}
func TestUnion_BothEmpty(t *testing.T) {
s1 := New[int]()
s2 := New[int]()
want := New[int]()
got := s1.Union(s2)
if !got.Equal(want) {
t.Errorf("Union() = %v; want %v", got, want)
}
}
func TestAdd(t *testing.T) {
want := New[int](1, 2, 3)
got := New[int]()
got.Add(1)
got.Add(2)
got.Add(3)
if !got.Equal(want) {
t.Errorf("Add() = %v; want %v", got, want)
}
}
func TestRemov(t *testing.T) {
want := New[int](1, 3)
got := New[int](1, 2, 3)
got.Remove(2)
if !got.Equal(want) {
t.Errorf("Remove() = %v; want %v", got, want)
}
}
func TestContains(t *testing.T) {
s := New[int](1, 2, 3)
if !s.Contains(2) {
t.Errorf("Contains(2) = false; want true")
}
if s.Contains(4) {
t.Errorf("Contains(4) = true; want false")
}
}
func TestSize(t *testing.T) {
s := New[int](1, 2, 3, 4)
want := 4
got := s.Size()
if got != want {
t.Errorf("Size() = %d; want %d", got, want)
}
}
func TestToSlice(t *testing.T) {
s := New[int](1, 2, 3)
want := []int{1, 2, 3}
got := s.ToSlice()
mapping := make(map[int]bool)
for _, v := range got {
mapping[v] = true
}
for _, v := range want {
if !mapping[v] {
t.Errorf("ToSlice() missing element %d; got %v", v, got)
}
} }
} }

77
simpleset/set.go Normal file
View File

@ -0,0 +1,77 @@
package simpleset
import "maps"
type Set[T comparable] struct {
contents map[T]struct{}
}
func New[T comparable](elements ...T) *Set[T] {
s := &Set[T]{
contents: make(map[T]struct{}),
}
for _, e := range elements {
s.Add(e)
}
return s
}
func (s *Set[T]) Add(element T) {
s.contents[element] = struct{}{}
}
func (s *Set[T]) Remove(element T) {
delete(s.contents, element)
}
func (s *Set[T]) Contains(element T) bool {
_, exists := s.contents[element]
return exists
}
func (s *Set[T]) Size() int {
return len(s.contents)
}
func (s *Set[T]) ToSlice() []T {
elements := make([]T, 0, len(s.contents))
for e := range s.contents {
elements = append(elements, e)
}
return elements
}
func (s *Set[T]) Equal(other *Set[T]) bool {
return maps.Equal(s.contents, other.contents)
}
func (s *Set[T]) Union(other *Set[T]) *Set[T] {
result := New[T]()
for e := range s.contents {
result.Add(e)
}
for e := range other.contents {
result.Add(e)
}
return result
}
func (s *Set[T]) Intersection(other *Set[T]) *Set[T] {
result := New[T]()
for e := range s.contents {
if other.Contains(e) {
result.Add(e)
}
}
return result
}
func (s *Set[T]) Difference(other *Set[T]) *Set[T] {
result := New[T]()
for e := range s.contents {
if !other.Contains(e) {
result.Add(e)
}
}
return result
}

196
simpleset/set_test.go Normal file
View File

@ -0,0 +1,196 @@
package simpleset
import "testing"
func TestIntersection(t *testing.T) {
s1 := New[int](1, 2, 3, 4)
s2 := New[int](3, 4, 5, 6)
want := New[int](3, 4)
got := s1.Intersection(s2)
if !got.Equal(want) {
t.Errorf("Intersection() = %v; want %v", got, want)
}
}
func TestIntersection_Empty(t *testing.T) {
s1 := New[int](1, 2)
s2 := New[int](3, 4)
want := New[int]()
got := s1.Intersection(s2)
if !got.Equal(want) {
t.Errorf("Intersection() = %v; want %v", got, want)
}
}
func TestIntersection_Identical(t *testing.T) {
s1 := New[int](1, 2, 3)
s2 := New[int](1, 2, 3)
want := New[int](1, 2, 3)
got := s1.Intersection(s2)
if !got.Equal(want) {
t.Errorf("Intersection() = %v; want %v", got, want)
}
}
func TestIntersection_Subset(t *testing.T) {
s1 := New[int](1, 2, 3, 4, 5)
s2 := New[int](2, 3)
want := New[int](2, 3)
got := s1.Intersection(s2)
if !got.Equal(want) {
t.Errorf("Intersection() = %v; want %v", got, want)
}
}
func TestIntersection_EmptySet(t *testing.T) {
s1 := New[int](1, 2, 3)
s2 := New[int]()
want := New[int]()
got := s1.Intersection(s2)
if !got.Equal(want) {
t.Errorf("Intersection() = %v; want %v", got, want)
}
}
func TestIntersection_BothEmpty(t *testing.T) {
s1 := New[int]()
s2 := New[int]()
want := New[int]()
got := s1.Intersection(s2)
if !got.Equal(want) {
t.Errorf("Intersection() = %v; want %v", got, want)
}
}
func TestDifference(t *testing.T) {
s1 := New[int](1, 2, 3, 4)
s2 := New[int](3, 4, 5, 6)
want := New[int](1, 2)
got := s1.Difference(s2)
if !got.Equal(want) {
t.Errorf("Difference() = %v; want %v", got, want)
}
}
func TestDifference_Empty(t *testing.T) {
s1 := New[int](1, 2)
s2 := New[int](1, 2)
want := New[int]()
got := s1.Difference(s2)
if !got.Equal(want) {
t.Errorf("Difference() = %v; want %v", got, want)
}
}
func TestDifference_Subset(t *testing.T) {
s1 := New[int](1, 2, 3, 4, 5)
s2 := New[int](2, 3)
want := New[int](1, 4, 5)
got := s1.Difference(s2)
if !got.Equal(want) {
t.Errorf("Difference() = %v; want %v", got, want)
}
}
func TestDifference_EmptySet(t *testing.T) {
s1 := New[int](1, 2, 3)
s2 := New[int]()
want := New[int](1, 2, 3)
got := s1.Difference(s2)
if !got.Equal(want) {
t.Errorf("Difference() = %v; want %v", got, want)
}
}
func TestDifference_BothEmpty(t *testing.T) {
s1 := New[int]()
s2 := New[int]()
want := New[int]()
got := s1.Difference(s2)
if !got.Equal(want) {
t.Errorf("Difference() = %v; want %v", got, want)
}
}
func TestUnion(t *testing.T) {
s1 := New[int](1, 2, 3)
s2 := New[int](3, 4, 5)
want := New[int](1, 2, 3, 4, 5)
got := s1.Union(s2)
if !got.Equal(want) {
t.Errorf("Union() = %v; want %v", got, want)
}
}
func TestUnion_EmptySet(t *testing.T) {
s1 := New[int](1, 2, 3)
s2 := New[int]()
want := New[int](1, 2, 3)
got := s1.Union(s2)
if !got.Equal(want) {
t.Errorf("Union() = %v; want %v", got, want)
}
}
func TestUnion_BothEmpty(t *testing.T) {
s1 := New[int]()
s2 := New[int]()
want := New[int]()
got := s1.Union(s2)
if !got.Equal(want) {
t.Errorf("Union() = %v; want %v", got, want)
}
}
func TestAdd(t *testing.T) {
want := New[int](1, 2, 3)
got := New[int]()
got.Add(1)
got.Add(2)
got.Add(3)
if !got.Equal(want) {
t.Errorf("Add() = %v; want %v", got, want)
}
}
func TestRemov(t *testing.T) {
want := New[int](1, 3)
got := New[int](1, 2, 3)
got.Remove(2)
if !got.Equal(want) {
t.Errorf("Remove() = %v; want %v", got, want)
}
}
func TestContains(t *testing.T) {
s := New[int](1, 2, 3)
if !s.Contains(2) {
t.Errorf("Contains(2) = false; want true")
}
if s.Contains(4) {
t.Errorf("Contains(4) = true; want false")
}
}
func TestSize(t *testing.T) {
s := New[int](1, 2, 3, 4)
want := 4
got := s.Size()
if got != want {
t.Errorf("Size() = %d; want %d", got, want)
}
}
func TestToSlice(t *testing.T) {
s := New[int](1, 2, 3)
want := []int{1, 2, 3}
got := s.ToSlice()
mapping := make(map[int]bool)
for _, v := range got {
mapping[v] = true
}
for _, v := range want {
if !mapping[v] {
t.Errorf("ToSlice() missing element %d; got %v", v, got)
}
}
}

81
threadsafeset/set.go Normal file
View File

@ -0,0 +1,81 @@
package threadsafeset
import (
"sync"
"code.wmdillon.com/wmdillon/set/simpleset"
)
type Set[T comparable] struct {
mutex sync.RWMutex
set *simpleset.Set[T]
}
func New[T comparable](elements ...T) *Set[T] {
s := &Set[T]{
set: simpleset.New(elements...),
}
return s
}
func (s *Set[T]) Add(element T) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.set.Add(element)
}
func (s *Set[T]) Remove(element T) {
s.mutex.Lock()
defer s.mutex.Unlock()
s.set.Remove(element)
}
func (s *Set[T]) Contains(element T) bool {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.set.Contains(element)
}
func (s *Set[T]) Size() int {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.set.Size()
}
func (s *Set[T]) ToSlice() []T {
s.mutex.RLock()
defer s.mutex.RUnlock()
return s.set.ToSlice()
}
// only locks this.mutex, locking other.mutex is the responsibility
// of the caller.
func (this *Set[T]) Equal(other *Set[T]) bool {
this.mutex.RLock()
defer this.mutex.RUnlock()
return this.set.Equal(other.set)
}
// only locks this.mutex, locking other.mutex is the responsibility
// of the caller.
func (this *Set[T]) Union(other *Set[T]) *Set[T] {
this.mutex.RLock()
defer this.mutex.RUnlock()
return &Set[T]{set: this.set.Union(other.set)}
}
// only locks this.mutex, locking other.mutex is the responsibility
// of the caller.
func (this *Set[T]) Intersection(other *Set[T]) *Set[T] {
this.mutex.RLock()
defer this.mutex.RUnlock()
return &Set[T]{set: this.set.Intersection(other.set)}
}
// only locks this.mutex, locking other.mutex is the responsibility
// of the caller.
func (this *Set[T]) Difference(other *Set[T]) *Set[T] {
this.mutex.RLock()
defer this.mutex.RUnlock()
return &Set[T]{set: this.set.Difference(other.set)}
}

137
threadsafeset/set_test.go Normal file
View File

@ -0,0 +1,137 @@
package threadsafeset
import (
"bufio"
"context"
"math/rand/v2"
"os"
"runtime"
"slices"
"strings"
"sync"
"testing"
"time"
)
var Words = func() []string {
f, err := os.Open("../words")
if err != nil {
panic("error opening ../words: " + err.Error())
}
defer f.Close()
scanner := bufio.NewScanner(f)
results := make([]string, 0)
for scanner.Scan() {
if line := strings.TrimSpace(scanner.Text()); len(line) > 0 {
results = append(results, line)
}
}
if err := scanner.Err(); err != nil {
panic("error from scanner.Err(): " + err.Error())
}
return results
}()
func CloneWords() []string {
results := make([]string, 0, len(Words))
for _, word := range Words {
results = append(results, strings.Clone(word))
}
return results
}
func RandomWord() string {
return Words[rand.IntN(len(Words))]
}
func ContextNotDone(ctx context.Context) bool {
select {
case <-ctx.Done():
return false
default:
return true
}
}
func TestAdd(t *testing.T) {
set := New[string]()
var wg sync.WaitGroup
ctx, cancel := context.WithTimeout(t.Context(), time.Second*2)
defer cancel()
for range runtime.NumCPU() * 10 {
wg.Go(func() {
for i := 0; i < len(Words) && ContextNotDone(ctx); i++ {
set.Add(Words[i])
}
})
}
wg.Wait()
}
func TestContains(t *testing.T) {
set := New(Words...)
var wg sync.WaitGroup
ctx, cancel := context.WithTimeout(t.Context(), time.Second*2)
defer cancel()
for range runtime.NumCPU() * 10 {
wg.Go(func() {
for i := 0; i < len(Words) && ContextNotDone(ctx); i++ {
if !set.Contains(Words[i]) {
panic("error: " + Words[i] + " not found")
}
}
})
}
wg.Wait()
}
func TestRemove(t *testing.T) {
set := New(Words...)
var wg sync.WaitGroup
for range runtime.NumCPU() * 10 {
wg.Go(func() {
for _, word := range Words {
set.Remove(word)
}
})
}
wg.Wait()
}
func TestSize(t *testing.T) {
set := New[string]()
for index, word := range Words {
if want, got := index, set.Size(); want != got {
t.Fatalf("error: wanted size %d; got %d\n", want, got)
}
set.Add(word)
if want, got := index+1, set.Size(); want != got {
t.Fatalf("error: wanted size %d; got %d\n", want, got)
}
}
}
func TestToSlice(t *testing.T) {
want := CloneWords()
slices.Sort(want)
set := New(Words...)
got := set.ToSlice()
slices.Sort(got)
if !slices.Equal(want, got) {
t.Fatalf("error: mismatched ToSlice() output\n")
}
}
func BenchmarkAdd(b *testing.B) {
set := New[string]()
for b.Loop() {
set.Add(RandomWord())
}
}
func BenchmarkContains(b *testing.B) {
set := New(Words...)
for b.Loop() {
set.Contains(RandomWord())
}
}

104334
words Normal file

File diff suppressed because it is too large Load Diff