Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(set): provide a Set implementation. #34

Merged
merged 1 commit into from
Mar 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .idea/runConfigurations/Set_demo.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ om.Delete(k) // Idempotent: does not fail on nonexistent keys.
### Queues

```go
var e Element
q := queue.NewSliceQueue[Element](sizeHint)
q.Enqueue(e)
if lq, ok := q.(container.Countable); ok {
Expand All @@ -67,6 +68,22 @@ for i := 0; i < 2; i++ {
}
```

### Sets

```go
var e Element
s := set.NewTrivial[Element](sizeHint)
s.Add(e)
s.Add(e)
if cs, ok := q.(container.Countable); ok {
fmt.Printf("elements in set: %d\n", cs.Len()) // 1
}
for e := range s.Items() {
fmt.Fprintln(w, e)
}

```

### Stacks

```go
Expand Down
9 changes: 9 additions & 0 deletions cmd/set/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package main

import (
"os"
)

func main() {
os.Exit(realMain(os.Stdout))

Check warning on line 8 in cmd/set/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/set/main.go#L7-L8

Added lines #L7 - L8 were not covered by tests
}
37 changes: 37 additions & 0 deletions cmd/set/real_main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"fmt"
"io"

"github.com/fgm/container"
"github.com/fgm/container/set"
)

type Element int

// SizeHint is an indication of the maximum number of elements expected in the
// set. It is not a hard limit. Implementations may use it or not.
const sizeHint = 100

func realMain(w io.Writer) int {
var e Element = 42

s := set.NewTrivial[Element](sizeHint)
// Add squares.
for i := range e {
s.Add(i * i)
}
if cs, ok := s.(container.Countable); ok {
fmt.Fprintf(w, "elements in set: %d\n", cs.Len())
}
// Remove elements to show that we
// can also remove elements which are absent in the map.
for i := Element(0); i < 10; i++ {
del := i * i * i
ok := s.Remove(del)
fmt.Fprintf(w, "Element: %3v ok: %t\n", del, ok)
}

return 0
}
33 changes: 33 additions & 0 deletions cmd/set/real_main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package main

import (
"bytes"
"testing"

"github.com/google/go-cmp/cmp"
)

func TestRealMain(t *testing.T) {
var buf bytes.Buffer
exitCode := realMain(&buf)

if exitCode != 0 {
t.Fatalf("expected exit code 0, got %d", exitCode)
}

expectedOutput := `elements in set: 42
Element: 0 ok: true
Element: 1 ok: true
Element: 8 ok: false
Element: 27 ok: false
Element: 64 ok: true
Element: 125 ok: false
Element: 216 ok: false
Element: 343 ok: false
Element: 512 ok: false
Element: 729 ok: true
`
if buf.String() != expectedOutput {
t.Fatal(cmp.Diff(expectedOutput, buf.String()))
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ module github.com/fgm/container
go 1.23.7

require github.com/google/go-cmp v0.7.0

require golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
208 changes: 208 additions & 0 deletions set/trivial.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
package set

import (
"fmt"
"iter"
"strings"

"github.com/fgm/container"
)

type unit = struct{}

// Trivial is the textbook Go implementation of a Go set using generics.
//
// It is not concurrency-safe.
// For performance optimization, its union/intersection/difference operations
// may return the receiver or the argument instead of cloning.
type Trivial[E comparable] struct {
items map[E]unit
}

// String returns an idiomatic unordered representation of the set items.
func (s *Trivial[E]) String() string {
// Shortcut empty case.
if s == nil || len(s.items) == 0 {
return "{}"
}

var b strings.Builder
b.WriteByte('{')

// Use a separate counter to avoid trailing comma
// Use a separate counter to avoid trailing comma
i := 0
for item := range s.items {
if i > 0 {
b.WriteString(", ")
}
_, _ = fmt.Fprintf(&b, "%v", item)
i++
}

b.WriteByte('}')
return b.String()
}

// Len returns the number of items in the Set.
func (s *Trivial[E]) Len() int {
if s == nil {
return 0
}
return len(s.items)
}

// Add adds an item to the set. Returns true if the item was already present.
func (s *Trivial[E]) Add(item E) (found bool) {
if s == nil {
return false
}
found = s.Contains(item)
s.items[item] = unit{}
return found
}

// Remove removes an item from the set.
// It does not fail if the item was not present, and returns true if it was.
func (s *Trivial[E]) Remove(item E) (found bool) {
if s == nil {
return false
}
found = s.Contains(item)
delete(s.items, item)
return found
}

// Contains returns true if the item is present in the set.
func (s *Trivial[E]) Contains(item E) bool {
if s == nil {
return false
}
_, exists := s.items[item]
return exists
}

// Clear removes all items from the set and returns the number of items removed.
func (s *Trivial[E]) Clear() (count int) {
if s == nil {
return 0
}

count = s.Len()
s.items = make(map[E]unit)
return count
}

// Items returns an unordered iterator over the set'set elements.
func (s *Trivial[E]) Items() iter.Seq[E] {
if s == nil {
return func(yield func(E) bool) {}
}

return func(yield func(E) bool) {
for item := range s.items {
if !yield(item) {
break
}
}
}
}

// Union returns a new set containing elements present in either set.
//
// Note that it may return one of its arguments without creating a clone.
func (s *Trivial[E]) Union(other container.Set[E]) container.Set[E] {
// Shortcut degenerate cases.
if s == nil && other == nil {
return NewTrivial[E](0)
}
if s == nil {
return other
}
if other == nil {
return s
}
if s.Len() == 0 {
return other
}
if other, ok := other.(container.Countable); ok && other.Len() == 0 {
return s
}

// Non-degenerate case. It will be at least as long as the receiver.
result := &Trivial[E]{items: make(map[E]unit, s.Len())}

// Add all items from this set
for item := range s.items {
result.Add(item)
}

// Add all items from other set
for item := range other.Items() {
result.Add(item)
}

return result
}

// Intersection returns a new set containing elements present in both sets.
func (s *Trivial[E]) Intersection(other container.Set[E]) container.Set[E] {
// Shortcut degenerate cases.
if s == nil || other == nil {
return NewTrivial[E](0)
}
if s.Len() == 0 {
return s
}
if countable, ok := other.(container.Countable); ok && countable.Len() == 0 {
return other
}

// Non-degenerate case with size optimization
var result *Trivial[E]
if other, ok := other.(container.Countable); ok {
result = &Trivial[E]{items: make(map[E]unit, min(s.Len(), other.Len()))}
} else {
result = &Trivial[E]{items: make(map[E]unit)}
}

// Add items that exist in both sets
for item := range s.items {
if other.Contains(item) {
result.Add(item)
}
}

return result
}

// Difference returns a new set containing elements present in this set but not in the other.
func (s *Trivial[E]) Difference(other container.Set[E]) container.Set[E] {
// Shortcut degenerate cases.
if s == nil {
return NewTrivial[E](0)
}
if other == nil || s.Len() == 0 {
return s
}
if other, ok := other.(container.Countable); ok && other.Len() == 0 {
return s
}

// Non-degenerate case.
result := &Trivial[E]{items: make(map[E]unit, s.Len())}

// Add items that exist in this set but not in other
for item := range s.items {
if !other.Contains(item) {
result.Add(item)
}
}

return result
}

// NewTrivial returns a ready-for-use container.Set implemented by the Trivial type.
func NewTrivial[E comparable](sizeHint int) container.Set[E] {
return &Trivial[E]{make(map[E]unit, sizeHint)}
}
Loading