From 4eb253d5c0d5c7a10467d18ea24469ca125c4904 Mon Sep 17 00:00:00 2001 From: spekary Date: Thu, 23 Jan 2025 08:21:27 -0800 Subject: [PATCH] Added ordered set --- equaler.go | 2 +- set.go | 5 ++- set_ordered.go | 60 ++++++++++++++++++++++++++ set_ordered_test.go | 103 ++++++++++++++++++++++++++++++++++++++++++++ seti_test.go | 56 +++++++++++++++++------- 5 files changed, 208 insertions(+), 18 deletions(-) create mode 100644 set_ordered.go create mode 100644 set_ordered_test.go diff --git a/equaler.go b/equaler.go index af1f6d9..611d24f 100644 --- a/equaler.go +++ b/equaler.go @@ -4,7 +4,7 @@ package maps // various MapI like objects to determine if they are equal. // // In particular, if your Map has -// non-comparible values, like a slice, but you would still like to call Equal() on that +// non-comparable values, like a slice, but you would still like to call Equal() on that // map, define an Equal function on the values to do the comparison. For example: // // type mySlice []int diff --git a/set.go b/set.go index 09742bf..a4b2a83 100644 --- a/set.go +++ b/set.go @@ -148,6 +148,9 @@ func (m *Set[K]) UnmarshalBinary(data []byte) (err error) { // MarshalJSON implements the json.Marshaler interface to convert the map into a JSON object. func (m *Set[K]) MarshalJSON() (out []byte, err error) { + if m.Len() == 0 { + return []byte("[]"), nil + } return json.Marshal(m.Values()) } @@ -163,7 +166,7 @@ func (m *Set[K]) UnmarshalJSON(in []byte) (err error) { return } -// String returns the set as a string in a predictable way. +// String returns the set as a string. func (m *Set[K]) String() string { vals := slices.Clone(m.Values()) ret := "{" diff --git a/set_ordered.go b/set_ordered.go new file mode 100644 index 0000000..5ce05af --- /dev/null +++ b/set_ordered.go @@ -0,0 +1,60 @@ +package maps + +import ( + "cmp" + "encoding/json" + "iter" + "slices" +) + +// OrderedSet implements a set of values that will be returned sorted. +// +// Ordered sets are useful when in general you don't care about ordering, but +// you would still like the same values to be presented in the same order when +// they are asked for. Examples include test code, iterators, values stored in a database, +// or values that will be presented to a user. +type OrderedSet[K cmp.Ordered] struct { + Set[K] +} + +func NewOrderedSet[K cmp.Ordered](values ...K) *OrderedSet[K] { + s := new(OrderedSet[K]) + for _, k := range values { + s.Add(k) + } + return s +} + +// Range will range over the values in order. +func (m *OrderedSet[K]) Range(f func(k K) bool) { + if m == nil || m.items == nil { + return + } + values := m.Values() + for _, k := range values { + if !f(k) { + break + } + } +} + +// Values returns the values as a slice, in order. +func (m *OrderedSet[K]) Values() []K { + v := m.items.Keys() + slices.Sort(v) + return v +} + +// MarshalJSON implements the json.Marshaler interface to convert the map into a JSON object. +func (m *OrderedSet[K]) MarshalJSON() (out []byte, err error) { + if m.Len() == 0 { + return []byte("[]"), nil + } + return json.Marshal(m.Values()) +} + +// All returns an iterator over all the items in the set. Order is determinate. +func (m *OrderedSet[K]) All() iter.Seq[K] { + v := m.Values() + return slices.Values(v) +} diff --git a/set_ordered_test.go b/set_ordered_test.go new file mode 100644 index 0000000..2e69d0b --- /dev/null +++ b/set_ordered_test.go @@ -0,0 +1,103 @@ +package maps + +import ( + "cmp" + "encoding/gob" + "github.com/stretchr/testify/assert" + "testing" +) + +type orderedSetT = OrderedSet[string] +type orderedSetTI = SetI[string] + +func TestOrderedSet_SetI(t *testing.T) { + runSetITests[OrderedSet[string]](t, makeSetI[OrderedSet[string]]) +} + +func init() { + gob.Register(new(OrderedSet[string])) +} + +func TestOrderedSet_Values(t *testing.T) { + type testCase[K cmp.Ordered] struct { + name string + m *OrderedSet[K] + want []K + } + tests := []testCase[int]{ + {"none", NewOrderedSet[int](), []int(nil)}, + {"one", NewOrderedSet[int](1), []int{1}}, + {"three", NewOrderedSet[int](1, 2, 3), []int{1, 2, 3}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equalf(t, tt.want, tt.m.Values(), "Values()") + }) + } +} + +func TestOrderedSet_MarshalJSON(t *testing.T) { + type testCase[K cmp.Ordered] struct { + name string + m *OrderedSet[K] + wantOut string + wantErr bool + } + tests := []testCase[string]{ + {"zero", NewOrderedSet[string](), `[]`, false}, + {"one", NewOrderedSet("a"), `["a"]`, false}, + {"three", NewOrderedSet("a", "c", "b"), `["a","b","c"]`, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := tt.m.MarshalJSON() + gotOut := string(b) + assert.Equal(t, tt.wantErr, err != nil) + assert.Equalf(t, tt.wantOut, gotOut, "MarshalJSON()") + }) + } +} + +func TestOrderedSetAll(t *testing.T) { + set := NewOrderedSet[int]() + set.Add(5) + set.Add(3) + set.Add(8) + set.Add(1) + + iterator := set.All() + var result []int + + for v := range iterator { + result = append(result, v) + } + + expected := []int{1, 3, 5, 8} + assert.Equal(t, expected, result) +} + +func TestOrderedSet_Range(t *testing.T) { + type testCase[K cmp.Ordered] struct { + name string + m *OrderedSet[K] + expected []int + } + tests := []testCase[int]{ + {"none", NewOrderedSet[int](), []int(nil)}, + {"one", NewOrderedSet[int](1), []int{1}}, + {"three", NewOrderedSet[int](1, 2, 3), []int{1, 2, 3}}, + {"four", NewOrderedSet[int](4, 3, 2, 1), []int{1, 2, 3}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var values []int + var count int + tt.m.Range(func(i int) bool { + values = append(values, i) + count++ + return count < 3 + }) + assert.Equal(t, tt.expected, values) + }) + } +} diff --git a/seti_test.go b/seti_test.go index 9395144..1c5e898 100644 --- a/seti_test.go +++ b/seti_test.go @@ -103,28 +103,25 @@ func testSetHas(t *testing.T, f makeSetF) { func testSetRange(t *testing.T, f makeSetF) { tests := []struct { - name string - m setTI - expected int + name string + m setTI + expectedLen int }{ - {"0", f(), 0}, - {"1", f("a"), 1}, - {"2", f("a", "b"), 2}, - {"3", f("a", "b", "c"), 2}, + {"none", f(), 0}, + {"one", f("a"), 1}, + {"three", f("b", "a", "c"), 3}, + {"four", f("d", "a", "c", "b"), 3}, } for _, tt := range tests { t.Run("Range "+tt.name, func(t *testing.T) { - count := 0 - tt.m.Range(func(k string) bool { + var values []string + var count int + tt.m.Range(func(i string) bool { + values = append(values, i) count++ - if count > 1 { - return false - } - return true + return count < 3 }) - if count != tt.expected { - t.Errorf("Expected %d, got %d", tt.expected, count) - } + assert.Equal(t, tt.expectedLen, len(values)) }) } } @@ -199,6 +196,13 @@ func testSetMarshalJSON(t *testing.T, f makeSetF) { assert.NoError(t, err) // Note: The below output is what is produced, but isn't guaranteed. go seems to currently be sorting keys assert.Contains(t, string(s), `"a"`) + + m = f() + s, err = json.Marshal(m) + assert.NoError(t, err) + // Note: The below output is what is produced, but isn't guaranteed. go seems to currently be sorting keys + assert.Equal(t, string(s), "[]") + }) } @@ -212,6 +216,26 @@ func testSetUnmarshalJSON[M any](t *testing.T, f makeSetF) { m2 := i.(SetI[string]) assert.True(t, m2.Has("c")) + + b = []byte(`[]`) + + var m3 M + + json.Unmarshal(b, &m3) + i = &m3 + m4 := i.(SetI[string]) + + assert.Equal(t, 0, m4.Len()) + + b = []byte(`["d"]`) + + // Unmarshalling into an existing set should add values + json.Unmarshal(b, &m) + i = &m + m5 := i.(SetI[string]) + + assert.Equal(t, 4, m5.Len()) + } func testSetDelete(t *testing.T, f makeSetF) {