Skip to content
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
2 changes: 1 addition & 1 deletion equaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion set.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand All @@ -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 := "{"
Expand Down
60 changes: 60 additions & 0 deletions set_ordered.go
Original file line number Diff line number Diff line change
@@ -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)
}
103 changes: 103 additions & 0 deletions set_ordered_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
56 changes: 40 additions & 16 deletions seti_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
}
}
Expand Down Expand Up @@ -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), "[]")

})
}

Expand All @@ -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) {
Expand Down
Loading