diff --git a/go.mod b/go.mod --- a/go.mod +++ b/go.mod @@ -215,7 +215,6 @@ gopkg.in/warnings.v0 v0.1.2 // indirect gotest.tools/v3 v3.5.2 // indirect lukechampine.com/blake3 v1.4.1 // indirect - tangled.org/oppi.li/sets v0.0.0-20251210075238-d0656dd747b1 // indirect ) replace github.com/sergi/go-diff => github.com/sergi/go-diff v1.1.0 diff --git a/go.sum b/go.sum --- a/go.sum +++ b/go.sum @@ -726,9 +726,5 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= -tangled.org/oppi.li/sets v0.0.0-20251122035104-aea095ac29d9 h1:LD0RqSiHzo3HuiU0gTc0+0DE2z0PRye1x6D1SsbBYU0= -tangled.org/oppi.li/sets v0.0.0-20251122035104-aea095ac29d9/go.mod h1:C09MVBFQm4osZYBfo7V9kwGw8T5zYK2bYmpGbrG7Lec= -tangled.org/oppi.li/sets v0.0.0-20251210075238-d0656dd747b1 h1:6SeBKpo7JJvVwuj/6ocnr8Xh2NI4SX2cDpTNqhBIY68= -tangled.org/oppi.li/sets v0.0.0-20251210075238-d0656dd747b1/go.mod h1:p1Xz6tCV3Kxfqe4o6B4MzOr40gTrG3hwJqiZS9EX9LM= tangled.sh/oppi.li/go-gitdiff v0.8.2 h1:pASJJNWaFn6EmEIUNNjHZQ3stRu6BqTO2YyjKvTcxIc= tangled.sh/oppi.li/go-gitdiff v0.8.2/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE= diff --git a/sets/gen.go b/sets/gen.go new file mode 100644 --- /dev/null +++ b/sets/gen.go @@ -0,0 +1,31 @@ +package sets + +import ( + "math/rand" + "reflect" + "testing/quick" +) + +func (_ Set[T]) Generate(rand *rand.Rand, size int) reflect.Value { + s := New[T]() + + var zero T + itemType := reflect.TypeOf(zero) + + for { + if s.Len() >= size { + break + } + + item, ok := quick.Value(itemType, rand) + if !ok { + continue + } + + if val, ok := item.Interface().(T); ok { + s.Insert(val) + } + } + + return reflect.ValueOf(s) +} diff --git a/sets/readme.txt b/sets/readme.txt new file mode 100644 --- /dev/null +++ b/sets/readme.txt @@ -0,0 +1,35 @@ +sets +---- +set datastructure for go with generics and iterators. the +api is supposed to mimic rust's std::collections::HashSet api. + + s1 := sets.Collect(slices.Values([]int{1, 2, 3, 4})) + s2 := sets.Collect(slices.Values([]int{1, 2, 3, 4, 5, 6})) + + union := sets.Collect(s1.Union(s2)) + intersect := sets.Collect(s1.Intersection(s2)) + diff := sets.Collect(s1.Difference(s2)) + symdiff := sets.Collect(s1.SymmetricDifference(s2)) + + s1.Len() // 4 + s1.Contains(1) // true + s1.IsEmpty() // false + s1.IsSubset(s2) // true + s1.IsSuperset(s2) // false + s1.IsDisjoint(s2) // false + + if exists := s1.Insert(1); exists { + // already existed in set + } + + if existed := s1.Remove(1); existed { + // existed in set, now removed + } + + +testing +------- +includes property-based tests using the wonderful +testing/quick module! + + go test -v diff --git a/sets/set.go b/sets/set.go new file mode 100644 --- /dev/null +++ b/sets/set.go @@ -0,0 +1,174 @@ +package sets + +import ( + "iter" + "maps" +) + +type Set[T comparable] struct { + data map[T]struct{} +} + +func New[T comparable]() Set[T] { + return Set[T]{ + data: make(map[T]struct{}), + } +} + +func (s *Set[T]) Insert(item T) bool { + _, exists := s.data[item] + s.data[item] = struct{}{} + return !exists +} + +func Singleton[T comparable](item T) Set[T] { + n := New[T]() + _ = n.Insert(item) + return n +} + +func (s *Set[T]) Remove(item T) bool { + _, exists := s.data[item] + if exists { + delete(s.data, item) + } + return exists +} + +func (s Set[T]) Contains(item T) bool { + _, exists := s.data[item] + return exists +} + +func (s Set[T]) Len() int { + return len(s.data) +} + +func (s Set[T]) IsEmpty() bool { + return len(s.data) == 0 +} + +func (s *Set[T]) Clear() { + s.data = make(map[T]struct{}) +} + +func (s Set[T]) All() iter.Seq[T] { + return func(yield func(T) bool) { + for item := range s.data { + if !yield(item) { + return + } + } + } +} + +func (s Set[T]) Clone() Set[T] { + return Set[T]{ + data: maps.Clone(s.data), + } +} + +func (s Set[T]) Union(other Set[T]) iter.Seq[T] { + if s.Len() >= other.Len() { + return chain(s.All(), other.Difference(s)) + } else { + return chain(other.All(), s.Difference(other)) + } +} + +func chain[T any](seqs ...iter.Seq[T]) iter.Seq[T] { + return func(yield func(T) bool) { + for _, seq := range seqs { + for item := range seq { + if !yield(item) { + return + } + } + } + } +} + +func (s Set[T]) Intersection(other Set[T]) iter.Seq[T] { + return func(yield func(T) bool) { + for item := range s.data { + if other.Contains(item) { + if !yield(item) { + return + } + } + } + } +} + +func (s Set[T]) Difference(other Set[T]) iter.Seq[T] { + return func(yield func(T) bool) { + for item := range s.data { + if !other.Contains(item) { + if !yield(item) { + return + } + } + } + } +} + +func (s Set[T]) SymmetricDifference(other Set[T]) iter.Seq[T] { + return func(yield func(T) bool) { + for item := range s.data { + if !other.Contains(item) { + if !yield(item) { + return + } + } + } + for item := range other.data { + if !s.Contains(item) { + if !yield(item) { + return + } + } + } + } +} + +func (s Set[T]) IsSubset(other Set[T]) bool { + for item := range s.data { + if !other.Contains(item) { + return false + } + } + return true +} + +func (s Set[T]) IsSuperset(other Set[T]) bool { + return other.IsSubset(s) +} + +func (s Set[T]) IsDisjoint(other Set[T]) bool { + for item := range s.data { + if other.Contains(item) { + return false + } + } + return true +} + +func (s Set[T]) Equal(other Set[T]) bool { + if s.Len() != other.Len() { + return false + } + for item := range s.data { + if !other.Contains(item) { + return false + } + } + return true +} + +func Collect[T comparable](seq iter.Seq[T]) Set[T] { + result := New[T]() + for item := range seq { + result.Insert(item) + } + return result +} diff --git a/sets/set_test.go b/sets/set_test.go new file mode 100644 --- /dev/null +++ b/sets/set_test.go @@ -0,0 +1,411 @@ +package sets + +import ( + "slices" + "testing" + "testing/quick" +) + +func TestNew(t *testing.T) { + s := New[int]() + if s.Len() != 0 { + t.Errorf("New set should be empty, got length %d", s.Len()) + } + if !s.IsEmpty() { + t.Error("New set should be empty") + } +} + +func TestFromSlice(t *testing.T) { + s := Collect(slices.Values([]int{1, 2, 3, 2, 1})) + if s.Len() != 3 { + t.Errorf("Expected length 3, got %d", s.Len()) + } + if !s.Contains(1) || !s.Contains(2) || !s.Contains(3) { + t.Error("Set should contain all unique elements from slice") + } +} + +func TestInsert(t *testing.T) { + s := New[string]() + + if !s.Insert("hello") { + t.Error("First insert should return true") + } + if s.Insert("hello") { + t.Error("Duplicate insert should return false") + } + if s.Len() != 1 { + t.Errorf("Expected length 1, got %d", s.Len()) + } +} + +func TestRemove(t *testing.T) { + s := Collect(slices.Values([]int{1, 2, 3})) + + if !s.Remove(2) { + t.Error("Remove existing element should return true") + } + if s.Remove(2) { + t.Error("Remove non-existing element should return false") + } + if s.Contains(2) { + t.Error("Element should be removed") + } + if s.Len() != 2 { + t.Errorf("Expected length 2, got %d", s.Len()) + } +} + +func TestContains(t *testing.T) { + s := Collect(slices.Values([]int{1, 2, 3})) + + if !s.Contains(1) { + t.Error("Should contain 1") + } + if s.Contains(4) { + t.Error("Should not contain 4") + } +} + +func TestClear(t *testing.T) { + s := Collect(slices.Values([]int{1, 2, 3})) + s.Clear() + + if !s.IsEmpty() { + t.Error("Set should be empty after clear") + } + if s.Len() != 0 { + t.Errorf("Expected length 0, got %d", s.Len()) + } +} + +func TestIterator(t *testing.T) { + s := Collect(slices.Values([]int{1, 2, 3})) + var items []int + + for item := range s.All() { + items = append(items, item) + } + + slices.Sort(items) + expected := []int{1, 2, 3} + if !slices.Equal(items, expected) { + t.Errorf("Expected %v, got %v", expected, items) + } +} + +func TestClone(t *testing.T) { + s1 := Collect(slices.Values([]int{1, 2, 3})) + s2 := s1.Clone() + + if !s1.Equal(s2) { + t.Error("Cloned set should be equal to original") + } + + s2.Insert(4) + if s1.Contains(4) { + t.Error("Modifying clone should not affect original") + } +} + +func TestUnion(t *testing.T) { + s1 := Collect(slices.Values([]int{1, 2})) + s2 := Collect(slices.Values([]int{2, 3})) + + result := Collect(s1.Union(s2)) + expected := Collect(slices.Values([]int{1, 2, 3})) + + if !result.Equal(expected) { + t.Errorf("Expected %v, got %v", expected, result) + } +} + +func TestIntersection(t *testing.T) { + s1 := Collect(slices.Values([]int{1, 2, 3})) + s2 := Collect(slices.Values([]int{2, 3, 4})) + + expected := Collect(slices.Values([]int{2, 3})) + result := Collect(s1.Intersection(s2)) + + if !result.Equal(expected) { + t.Errorf("Expected %v, got %v", expected, result) + } +} + +func TestDifference(t *testing.T) { + s1 := Collect(slices.Values([]int{1, 2, 3})) + s2 := Collect(slices.Values([]int{2, 3, 4})) + + expected := Collect(slices.Values([]int{1})) + result := Collect(s1.Difference(s2)) + + if !result.Equal(expected) { + t.Errorf("Expected %v, got %v", expected, result) + } +} + +func TestSymmetricDifference(t *testing.T) { + s1 := Collect(slices.Values([]int{1, 2, 3})) + s2 := Collect(slices.Values([]int{2, 3, 4})) + + expected := Collect(slices.Values([]int{1, 4})) + result := Collect(s1.SymmetricDifference(s2)) + + if !result.Equal(expected) { + t.Errorf("Expected %v, got %v", expected, result) + } +} + +func TestSymmetricDifferenceCommutativeProperty(t *testing.T) { + s1 := Collect(slices.Values([]int{1, 2, 3})) + s2 := Collect(slices.Values([]int{2, 3, 4})) + + result1 := Collect(s1.SymmetricDifference(s2)) + result2 := Collect(s2.SymmetricDifference(s1)) + + if !result1.Equal(result2) { + t.Errorf("Expected %v, got %v", result1, result2) + } +} + +func TestIsSubset(t *testing.T) { + s1 := Collect(slices.Values([]int{1, 2})) + s2 := Collect(slices.Values([]int{1, 2, 3})) + + if !s1.IsSubset(s2) { + t.Error("s1 should be subset of s2") + } + if s2.IsSubset(s1) { + t.Error("s2 should not be subset of s1") + } +} + +func TestIsSuperset(t *testing.T) { + s1 := Collect(slices.Values([]int{1, 2, 3})) + s2 := Collect(slices.Values([]int{1, 2})) + + if !s1.IsSuperset(s2) { + t.Error("s1 should be superset of s2") + } + if s2.IsSuperset(s1) { + t.Error("s2 should not be superset of s1") + } +} + +func TestIsDisjoint(t *testing.T) { + s1 := Collect(slices.Values([]int{1, 2})) + s2 := Collect(slices.Values([]int{3, 4})) + s3 := Collect(slices.Values([]int{2, 3})) + + if !s1.IsDisjoint(s2) { + t.Error("s1 and s2 should be disjoint") + } + if s1.IsDisjoint(s3) { + t.Error("s1 and s3 should not be disjoint") + } +} + +func TestEqual(t *testing.T) { + s1 := Collect(slices.Values([]int{1, 2, 3})) + s2 := Collect(slices.Values([]int{3, 2, 1})) + s3 := Collect(slices.Values([]int{1, 2})) + + if !s1.Equal(s2) { + t.Error("s1 and s2 should be equal") + } + if s1.Equal(s3) { + t.Error("s1 and s3 should not be equal") + } +} + +func TestCollect(t *testing.T) { + s1 := Collect(slices.Values([]int{1, 2})) + s2 := Collect(slices.Values([]int{2, 3})) + + unionSet := Collect(s1.Union(s2)) + if unionSet.Len() != 3 { + t.Errorf("Expected union set length 3, got %d", unionSet.Len()) + } + if !unionSet.Contains(1) || !unionSet.Contains(2) || !unionSet.Contains(3) { + t.Error("Union set should contain 1, 2, and 3") + } + + diffSet := Collect(s1.Difference(s2)) + if diffSet.Len() != 1 { + t.Errorf("Expected difference set length 1, got %d", diffSet.Len()) + } + if !diffSet.Contains(1) { + t.Error("Difference set should contain 1") + } +} + +func TestPropertySingleonLen(t *testing.T) { + f := func(item int) bool { + single := Singleton(item) + return single.Len() == 1 + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestPropertyInsertIdempotent(t *testing.T) { + f := func(s Set[int], item int) bool { + clone := s.Clone() + + clone.Insert(item) + firstLen := clone.Len() + + clone.Insert(item) + secondLen := clone.Len() + + return firstLen == secondLen + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestPropertyUnionCommutative(t *testing.T) { + f := func(s1 Set[int], s2 Set[int]) bool { + union1 := Collect(s1.Union(s2)) + union2 := Collect(s2.Union(s1)) + return union1.Equal(union2) + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestPropertyIntersectionCommutative(t *testing.T) { + f := func(s1 Set[int], s2 Set[int]) bool { + inter1 := Collect(s1.Intersection(s2)) + inter2 := Collect(s2.Intersection(s1)) + return inter1.Equal(inter2) + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestPropertyCloneEquals(t *testing.T) { + f := func(s Set[int]) bool { + clone := s.Clone() + return s.Equal(clone) + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestPropertyIntersectionIsSubset(t *testing.T) { + f := func(s1 Set[int], s2 Set[int]) bool { + inter := Collect(s1.Intersection(s2)) + return inter.IsSubset(s1) && inter.IsSubset(s2) + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestPropertyUnionIsSuperset(t *testing.T) { + f := func(s1 Set[int], s2 Set[int]) bool { + union := Collect(s1.Union(s2)) + return union.IsSuperset(s1) && union.IsSuperset(s2) + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestPropertyDifferenceDisjoint(t *testing.T) { + f := func(s1 Set[int], s2 Set[int]) bool { + diff := Collect(s1.Difference(s2)) + return diff.IsDisjoint(s2) + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestPropertySymmetricDifferenceCommutative(t *testing.T) { + f := func(s1 Set[int], s2 Set[int]) bool { + symDiff1 := Collect(s1.SymmetricDifference(s2)) + symDiff2 := Collect(s2.SymmetricDifference(s1)) + return symDiff1.Equal(symDiff2) + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestPropertyRemoveWorks(t *testing.T) { + f := func(s Set[int], item int) bool { + clone := s.Clone() + clone.Insert(item) + clone.Remove(item) + return !clone.Contains(item) + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestPropertyClearEmpty(t *testing.T) { + f := func(s Set[int]) bool { + s.Clear() + return s.IsEmpty() && s.Len() == 0 + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestPropertyIsSubsetReflexive(t *testing.T) { + f := func(s Set[int]) bool { + return s.IsSubset(s) + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} + +func TestPropertyDeMorganUnion(t *testing.T) { + f := func(s1 Set[int], s2 Set[int], universe Set[int]) bool { + // create a universe that contains both sets + u := universe.Clone() + for item := range s1.All() { + u.Insert(item) + } + for item := range s2.All() { + u.Insert(item) + } + + // (A u B)' = A' n B' + union := Collect(s1.Union(s2)) + complementUnion := Collect(u.Difference(union)) + + complementS1 := Collect(u.Difference(s1)) + complementS2 := Collect(u.Difference(s2)) + intersectionComplements := Collect(complementS1.Intersection(complementS2)) + + return complementUnion.Equal(intersectionComplements) + } + + if err := quick.Check(f, nil); err != nil { + t.Error(err) + } +} diff --git a/appview/notify/db/db.go b/appview/notify/db/db.go --- a/appview/notify/db/db.go +++ b/appview/notify/db/db.go @@ -12,7 +12,7 @@ "tangled.org/core/appview/notify" "tangled.org/core/idresolver" "tangled.org/core/orm" - "tangled.org/oppi.li/sets" + "tangled.org/core/sets" ) const (