commit e2bc7888861a417b2f08e7c8df6f3a6daba61596 Author: William Dillon Date: Fri Nov 28 13:02:56 2025 -0500 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..f5c98d9 --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# set + +A small, generic Go set implementation for ordered types (numbers, strings, etc.). + +This package provides a lightweight `Set[T]` built on Go maps, with common +set operations such as union, intersection, difference, and helpers to convert +to slices. It uses Go generics and requires ordered element types. + +## Features + +- Generic `Set[T]` where `T` is any `constraints.Ordered` type (e.g. `int`, + `string`, `float64`). +- Common set operations: `Union`, `Intersection`, `Difference`. +- Convenience methods: `Add`, `Remove`, `Contains`, `Size`, `ToSlice`, + `ToSortedSlice`, and `Equal`. + +## Requirements + +- Go 1.20+ (uses `maps` package and generics introduced in earlier versions; `maps.Equal` requires Go 1.20+). + +## Module & Dependencies + +- **Module path:** `code.wmdillon.com/wmdillon/settings` +- **Go toolchain:** `go 1.25.3` +- **Direct dependencies:** + - `golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39` + +To fetch dependencies for this module run: + +```bash +go mod download +``` + +## Installation + +Add the package to your module by importing it. Replace the import path with +your module path if the package is local to your repository. + +```go +import "your/module/path/set" +``` + +## Quick Example + +```go +package main + +import ( + "fmt" + "your/module/path/set" +) + +func main() { + // Create new sets of ints + s1 := set.New[int](1, 2, 3, 4) + s2 := set.New[int](3, 4, 5) + + // Union + u := s1.Union(s2) + fmt.Println("Union:", u.ToSortedSlice()) // [1 2 3 4 5] + + // Intersection + i := s1.Intersection(s2) + fmt.Println("Intersection:", i.ToSortedSlice()) // [3 4] + + // Difference + d := s1.Difference(s2) + fmt.Println("Difference:", d.ToSortedSlice()) // [1 2] + + // Add / Remove / Contains + s := set.New[int]() + s.Add(10) + s.Add(20) + fmt.Println("Contains 20?", s.Contains(20)) + s.Remove(20) + fmt.Println("Size:", s.Size()) +} +``` + +## API Overview + +- `type Set[T constraints.Ordered] map[T]struct{}` +- `func New[T constraints.Ordered](elements ...T) Set[T]` +- `func (s Set[T]) Add(element T)` +- `func (s Set[T]) Remove(element T)` +- `func (s Set[T]) Contains(element T) bool` +- `func (s Set[T]) Size() int` +- `func (s Set[T]) ToSlice() []T` +- `func (s Set[T]) ToSortedSlice() []T` — returns a sorted slice (ascending order) +- `func (s Set[T]) Union(other Set[T]) Set[T]` +- `func (s Set[T]) Intersection(other Set[T]) Set[T]` +- `func (s Set[T]) Difference(other Set[T]) Set[T]` +- `func (s Set[T]) Equal(other Set[T]) bool` + +## Running Tests + +From the package directory run: + +```bash +go test ./... -v +``` + +The repository includes unit tests (`set_test.go`) demonstrating expected +behavior for union, intersection, difference, membership, and conversions. + +## Notes + +- Because the implementation uses `constraints.Ordered`, only ordered types are + supported (numbers, strings). If you need sets for non-ordered types (e.g. + structs) you can adapt the code to use `comparable` and provide a custom + ordering for `ToSortedSlice`. +- The `Equal` method uses `maps.Equal` from the standard library to compare + underlying maps. + +If you'd like, I can update the README with an import path (module name), +add more examples for strings or custom types, or include benchmarks. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7aaa832 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module code.wmdillon.com/wmdillon/settings + +go 1.25.3 + +require golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a3d0f52 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +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= diff --git a/set.go b/set.go new file mode 100644 index 0000000..3618287 --- /dev/null +++ b/set.go @@ -0,0 +1,86 @@ +package set + +import ( + "maps" + "sort" + + "golang.org/x/exp/constraints" +) + +type Set[T constraints.Ordered] map[T]struct{} + +func New[T constraints.Ordered](elements ...T) Set[T] { + s := Set[T]{} + for _, e := range elements { + s.Add(e) + } + return s +} + +func (s Set[T]) Equal(other Set[T]) bool { + return maps.Equal(s, other) +} + +func (s Set[T]) Add(element T) { + s[element] = struct{}{} +} + +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]) ToSortedSlice() []T { + elements := s.ToSlice() + sort.Slice(elements, func(i, j int) bool { + return elements[i] < elements[j] + }) + 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 +} diff --git a/set_test.go b/set_test.go new file mode 100644 index 0000000..6cf2ed3 --- /dev/null +++ b/set_test.go @@ -0,0 +1,208 @@ +package set + +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) + } + } +} + +func TestToSortedSlice(t *testing.T) { + s := New[int](3, 1, 2) + want := []int{1, 2, 3} + got := s.ToSortedSlice() + for i := range want { + if got[i] != want[i] { + t.Errorf("ToSortedSlice() = %v; want %v", got, want) + break + } + } +}