diff --git a/README.md b/README.md index 6238ce8..3525bde 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,23 @@ Len() int ToSlice() []T ``` +## orderedset package + +This package provides an OrderedSet struct with the following methods: + +```go +Add(values ...T) +AddSlice(slice []T) +Remove(value T) +RemoveSlice(slice []T) +Includes(value T) bool +Len() int +ToSliceFromOldest() []T +ToSliceFromNewest() []T +``` + +The difference to Set is that the insertion order of the values is preserved. + ## maps package Provides some helper methods for maps: diff --git a/go.mod b/go.mod index 501b32b..e268730 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,14 @@ module github.com/jesseduffield/generics go 1.18 -require golang.org/x/exp v0.0.0-20220317015231-48e79f11773a +require ( + github.com/wk8/go-ordered-map/v2 v2.1.8 + golang.org/x/exp v0.0.0-20220317015231-48e79f11773a +) + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index b1af872..c7577db 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,18 @@ +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= golang.org/x/exp v0.0.0-20220317015231-48e79f11773a h1:DAzrdbxsb5tXNOhMCSwF7ZdfMbW46hE9fSVO6BsmUZM= golang.org/x/exp v0.0.0-20220317015231-48e79f11773a/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/maps/maps.go b/maps/maps.go index 9d41a33..26d9c32 100644 --- a/maps/maps.go +++ b/maps/maps.go @@ -19,7 +19,7 @@ func Values[Key comparable, Value any](m map[Key]Value) []Value { func TransformValues[Key comparable, Value any, NewValue any]( m map[Key]Value, fn func(Value) NewValue, ) map[Key]NewValue { - output := make(map[Key]NewValue) + output := make(map[Key]NewValue, len(m)) for key, value := range m { output[key] = fn(value) } @@ -27,7 +27,7 @@ func TransformValues[Key comparable, Value any, NewValue any]( } func TransformKeys[Key comparable, Value any, NewKey comparable](m map[Key]Value, fn func(Key) NewKey) map[NewKey]Value { - output := make(map[NewKey]Value) + output := make(map[NewKey]Value, len(m)) for key, value := range m { output[fn(key)] = value } diff --git a/orderedset/orderedset.go b/orderedset/orderedset.go new file mode 100644 index 0000000..ed5c95b --- /dev/null +++ b/orderedset/orderedset.go @@ -0,0 +1,65 @@ +package orderedset + +import ( + orderedmap "github.com/wk8/go-ordered-map/v2" +) + +type OrderedSet[T comparable] struct { + om *orderedmap.OrderedMap[T, bool] +} + +func New[T comparable]() *OrderedSet[T] { + return &OrderedSet[T]{om: orderedmap.New[T, bool]()} +} + +func NewFromSlice[T comparable](slice []T) *OrderedSet[T] { + result := &OrderedSet[T]{om: orderedmap.New[T, bool](len(slice))} + result.Add(slice...) + return result +} + +func (os *OrderedSet[T]) Add(values ...T) { + for _, value := range values { + os.om.Set(value, true) + } +} + +func (os *OrderedSet[T]) Remove(value T) { + os.om.Delete(value) +} + +func (os *OrderedSet[T]) RemoveSlice(slice []T) { + for _, value := range slice { + os.Remove(value) + } +} + +func (os *OrderedSet[T]) Includes(value T) bool { + return os.om.Value(value) +} + +func (os *OrderedSet[T]) Len() int { + return os.om.Len() +} + +func (os *OrderedSet[T]) ToSliceFromOldest() []T { + // TODO: can be simplified to + // return os.om.KeysFromOldest() + // when we update to a newer version of go-ordered-map + result := make([]T, 0, os.Len()) + for pair := os.om.Oldest(); pair != nil; pair = pair.Next() { + result = append(result, pair.Key) + } + return result +} + +func (os *OrderedSet[T]) ToSliceFromNewest() []T { + // TODO: can be simplified to + // return os.om.KeysFromNewest() + // when we update to a newer version of go-ordered-map + result := make([]T, 0, os.Len()) + for pair := os.om.Newest(); pair != nil; pair = pair.Prev() { + result = append(result, pair.Key) + } + return result +} diff --git a/orderedset/orderedset_test.go b/orderedset/orderedset_test.go new file mode 100644 index 0000000..63a39ae --- /dev/null +++ b/orderedset/orderedset_test.go @@ -0,0 +1,103 @@ +package orderedset + +import ( + "testing" + + "github.com/jesseduffield/generics/internal/testutils" +) + +func TestAddIncludes(t *testing.T) { + os := New[int]() + os.Add(1) + if !os.Includes(1) { + t.Errorf("Add(1) failed: Includes(1) returned false") + } + + if os.Includes(0) { + t.Errorf("Add(1) failed: Includes(0) returned true") + } +} + +func TestAddSliceIncludes(t *testing.T) { + os := New[int]() + os.Add(1, 2) + if !os.Includes(1) { + t.Errorf("AddSlice failed: Includes(1) returned false") + } + + if !os.Includes(2) { + t.Errorf("AddSlice failed: Includes(2) returned false") + } + + if os.Includes(3) { + t.Errorf("AddSlice failed: Includes(3) returned true") + } +} + +func TestRemoveIncludes(t *testing.T) { + os := NewFromSlice([]int{1, 2}) + os.Remove(1) + if os.Includes(1) { + t.Errorf("Remove failed: Includes(1) returned true") + } + + if !os.Includes(2) { + t.Errorf("Remove failed: Includes(2) returned false") + } +} + +func TestRemoveSliceIncludes(t *testing.T) { + os := NewFromSlice([]int{1, 2, 3}) + os.RemoveSlice([]int{1, 2}) + if os.Includes(1) { + t.Errorf("Remove failed: Includes(1) returned true") + } + + if os.Includes(2) { + t.Errorf("Remove failed: Includes(2) returned true") + } + + if !os.Includes(3) { + t.Errorf("Remove failed: Includes(3) returned false") + } +} + +func TestNewFromSlice(t *testing.T) { + os := NewFromSlice([]int{1, 2}) + if !os.Includes(1) { + t.Errorf("NewFromSlice failed: Includes(1) returned false") + } + + if !os.Includes(2) { + t.Errorf("NewFromSlice failed: Includes(2) returned false") + } + + if os.Includes(3) { + t.Errorf("NewFromSlice failed: Includes(3) returned true") + } +} + +func TestLen(t *testing.T) { + set := NewFromSlice([]int{}) + if set.Len() != 0 { + t.Errorf("Len() = %v, expected %v", set.Len(), 0) + } + + set = NewFromSlice([]int{1}) + if set.Len() != 1 { + t.Errorf("Len() = %v, expected %v", set.Len(), 1) + } +} + +func TestToSlice(t *testing.T) { + set := New[int]() + set.Add(1) + set.Add(3) + set.Add(2) + + slice := set.ToSliceFromOldest() + testutils.ExpectSlice(t, []int{1, 3, 2}, slice) + + slice = set.ToSliceFromNewest() + testutils.ExpectSlice(t, []int{2, 3, 1}, slice) +} diff --git a/set/set.go b/set/set.go index 0f0f4be..5b8f009 100644 --- a/set/set.go +++ b/set/set.go @@ -11,12 +11,9 @@ func New[T comparable]() *Set[T] { } func NewFromSlice[T comparable](slice []T) *Set[T] { - hashMap := make(map[T]bool) - for _, value := range slice { - hashMap[value] = true - } - - return &Set[T]{hashMap: hashMap} + result := &Set[T]{hashMap: make(map[T]bool, len(slice))} + result.Add(slice...) + return result } func (s *Set[T]) Add(values ...T) { diff --git a/slices/slices.go b/slices/slices.go index 5bfb199..f5c1463 100644 --- a/slices/slices.go +++ b/slices/slices.go @@ -112,7 +112,7 @@ func MapInPlace[T any](slice []T, f func(T) T) { // Produces a new slice, leaves the input slice untouched. func Filter[T any](slice []T, test func(T) bool) []T { - result := make([]T, 0) + result := make([]T, 0, len(slice)) for _, element := range slice { if test(element) { result = append(result, element) @@ -134,7 +134,7 @@ func FilterWithIndex[T any](slice []T, f func(T, int) bool) []T { } func TryFilter[T any](slice []T, test func(T) (bool, error)) ([]T, error) { - result := make([]T, 0) + result := make([]T, 0, len(slice)) for _, element := range slice { ok, err := test(element) if err != nil { @@ -148,7 +148,7 @@ func TryFilter[T any](slice []T, test func(T) (bool, error)) ([]T, error) { } func TryFilterWithIndex[T any](slice []T, test func(T, int) (bool, error)) ([]T, error) { - result := make([]T, 0) + result := make([]T, 0, len(slice)) for i, element := range slice { ok, err := test(element, i) if err != nil {