├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── maps ├── map.go └── map_test.go └── slices ├── slice.go └── slice_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Mikhail Swift 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-collections 2 | 3 | Generic utility functions for dealing with slices and maps in go. 4 | 5 | This was created primarily for me to learn Generics with Go 1.18's release. I've since added in iterators here to also learn those. 6 | **There is likely no reason to use this over the Go stdlib's slices and maps packages that have since been created.** 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module collections 2 | 3 | go 1.24 4 | 5 | require github.com/stretchr/testify v1.7.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.0 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 12 | -------------------------------------------------------------------------------- /maps/map.go: -------------------------------------------------------------------------------- 1 | package maps 2 | 3 | import "iter" 4 | 5 | // Keys returns a slice of all keys in m 6 | func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K] { 7 | return func(yield func(K) bool) { 8 | for k := range m { 9 | if !yield(k) { 10 | return 11 | } 12 | } 13 | } 14 | } 15 | 16 | // Values returns a slice of all values in m 17 | func Values[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[V] { 18 | return func(yield func(V) bool) { 19 | for _, v := range m { 20 | if !yield(v) { 21 | return 22 | } 23 | } 24 | } 25 | } 26 | 27 | // Union returns a new map of all key/value pairs in left and right. If a key exists 28 | // in both left and right the value in right will appear in the resultant map. 29 | func Union[Map ~map[K]V, K comparable, V any](left, right Map) Map { 30 | result := make(Map) 31 | for k, v := range left { 32 | result[k] = v 33 | } 34 | 35 | for k, v := range right { 36 | result[k] = v 37 | } 38 | 39 | return result 40 | } 41 | 42 | // UnionInPlace modifies left to include key/value pairs in right. If a key exists 43 | // in both left and right the value in right will be used. 44 | func UnionInPlace[Map ~map[K]V, K comparable, V any](left, right Map) Map { 45 | for k, v := range right { 46 | left[k] = v 47 | } 48 | 49 | return left 50 | } 51 | 52 | // Intersect returns a new map of key/value pairs where the key exists in both left and right. 53 | // The value from right will be used in the return map. 54 | func Intersect[Map ~map[K]V, K comparable, V any](left, right Map) Map { 55 | result := make(Map) 56 | for k := range left { 57 | if rv, ok := right[k]; ok { 58 | result[k] = rv 59 | } 60 | } 61 | 62 | return result 63 | } 64 | 65 | // Difference returns a new map of key/value pairs that only appear in left. 66 | func Difference[Map ~map[K]V, K comparable, V any](left, right Map) Map { 67 | result := make(Map) 68 | for k, v := range left { 69 | if _, ok := right[k]; !ok { 70 | result[k] = v 71 | } 72 | } 73 | 74 | return result 75 | } 76 | 77 | // YieldAll iterates over a provided iterator and returns a Map containing all elements from the iterator. 78 | func YieldAll[Map ~map[K]V, K comparable, V any](i iter.Seq2[K, V]) Map { 79 | result := make(Map) 80 | for k, v := range i { 81 | result[k] = v 82 | } 83 | 84 | return result 85 | } 86 | -------------------------------------------------------------------------------- /maps/map_test.go: -------------------------------------------------------------------------------- 1 | package maps 2 | 3 | import ( 4 | "collections/slices" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestKeysValues(t *testing.T) { 11 | m := map[int]string{ 12 | 1: "one", 13 | 2: "two", 14 | 3: "three", 15 | } 16 | 17 | assert.ElementsMatch(t, []int{1, 2, 3}, slices.YieldAll[[]int](Keys(m))) 18 | assert.ElementsMatch(t, []string{"one", "two", "three"}, slices.YieldAll[[]string](Values(m))) 19 | } 20 | 21 | func TestUnion(t *testing.T) { 22 | a := map[int]string{ 23 | 1: "one", 24 | 2: "two", 25 | 3: "three", 26 | } 27 | 28 | b := map[int]string{ 29 | 3: "threeb", 30 | 4: "four", 31 | 5: "five", 32 | } 33 | 34 | expected := map[int]string{ 35 | 1: "one", 36 | 2: "two", 37 | 3: "threeb", 38 | 4: "four", 39 | 5: "five", 40 | } 41 | 42 | assert.Equal(t, expected, Union(a, b)) 43 | assert.NotEqual(t, expected, a) 44 | inplace := UnionInPlace(a, b) 45 | assert.Equal(t, expected, inplace) 46 | assert.Equal(t, inplace, a) 47 | } 48 | 49 | func TestIntersect(t *testing.T) { 50 | a := map[int]string{ 51 | 1: "one", 52 | 2: "two", 53 | 3: "three", 54 | } 55 | 56 | b := map[int]string{ 57 | 3: "threeb", 58 | 4: "four", 59 | 5: "five", 60 | 1: "one", 61 | } 62 | 63 | expected := map[int]string{ 64 | 1: "one", 65 | 3: "threeb", 66 | } 67 | 68 | assert.Equal(t, expected, Intersect(a, b)) 69 | } 70 | 71 | func TestDifference(t *testing.T) { 72 | a := map[int]string{ 73 | 1: "one", 74 | 2: "two", 75 | 3: "three", 76 | } 77 | 78 | b := map[int]string{ 79 | 3: "threeb", 80 | 4: "four", 81 | 5: "five", 82 | 1: "one", 83 | } 84 | 85 | expected := map[int]string{ 86 | 2: "two", 87 | } 88 | 89 | assert.Equal(t, expected, Difference(a, b)) 90 | } 91 | -------------------------------------------------------------------------------- /slices/slice.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import "iter" 4 | 5 | // Filter returns a new slice consisting of all elements that pass the predicate function 6 | func Filter[TSlice ~[]T, T any](slice TSlice, predicate func(T) bool) iter.Seq[T] { 7 | return func(yield func(T) bool) { 8 | for _, t := range slice { 9 | if predicate(t) { 10 | if !yield(t) { 11 | return 12 | } 13 | } 14 | } 15 | } 16 | } 17 | 18 | // FirstMatch returns the first element that passes the predicate function. 19 | // If no element is found the zero value of the type will be returned. 20 | func FirstMatch[TSlice ~[]T, T any](slice TSlice, predicate func(T) bool) T { 21 | for _, t := range slice { 22 | if predicate(t) { 23 | return t 24 | } 25 | } 26 | 27 | var zero T 28 | return zero 29 | } 30 | 31 | // LastMatch returns the last element that passes the predicate function. 32 | // If no element is found the zero value of the type will be returned. 33 | func LastMatch[TSlice ~[]T, T any](slice TSlice, predicate func(T) bool) T { 34 | for i := len(slice) - 1; i >= 0; i-- { 35 | if predicate(slice[i]) { 36 | return slice[i] 37 | } 38 | } 39 | 40 | var zero T 41 | return zero 42 | } 43 | 44 | // AnyMatch returns true if any element passes the predicate function 45 | func AnyMatch[TSlice ~[]T, T any](slice TSlice, predicate func(T) bool) bool { 46 | for _, t := range slice { 47 | if predicate(t) { 48 | return true 49 | } 50 | } 51 | 52 | return false 53 | } 54 | 55 | // AllMatch returns true if all elements pass the predicate function 56 | func AllMatch[TSlice ~[]T, T any](slice TSlice, predicate func(T) bool) bool { 57 | for _, t := range slice { 58 | if !predicate(t) { 59 | return false 60 | } 61 | } 62 | 63 | return true 64 | } 65 | 66 | // Map returns a new slice where each element is the result of fn for the corresponding element in the original slice 67 | func Map[TSlice ~[]T, T any, U any](slice []T, fn func(T) U) []U { 68 | result := make([]U, len(slice)) 69 | for i, t := range slice { 70 | result[i] = fn(t) 71 | } 72 | 73 | return result 74 | } 75 | 76 | // Contains returns true if find appears in slice 77 | func Contains[TSlice ~[]T, T comparable](slice TSlice, find T) bool { 78 | for _, t := range slice { 79 | if t == find { 80 | return true 81 | } 82 | } 83 | 84 | return false 85 | } 86 | 87 | // IndexOf returns the index of find if it appears in slice. If find is not in slice, -1 will be returned. 88 | func IndexOf[TSlice ~[]T, T comparable](slice TSlice, find T) int { 89 | for i, t := range slice { 90 | if t == find { 91 | return i 92 | } 93 | } 94 | 95 | return -1 96 | } 97 | 98 | // GroupBy returns a map that is keyed by keySelector and contains a slice of elements returned by valSelector 99 | func GroupBy[TSlice ~[]T, T any, K comparable, V any](slice TSlice, keySelector func(T) K, valSelector func(T) V) map[K][]V { 100 | grouping := make(map[K][]V) 101 | for _, t := range slice { 102 | key := keySelector(t) 103 | grouping[key] = append(grouping[key], valSelector(t)) 104 | } 105 | 106 | return grouping 107 | } 108 | 109 | // ToSet returns a map keyed by keySelector and contains a value of an empty struct 110 | func ToSet[TSlice ~[]T, T any, K comparable](slice TSlice, keySelector func(T) K) iter.Seq2[K, struct{}] { 111 | return func(yield func(K, struct{}) bool) { 112 | for _, t := range slice { 113 | if !yield(keySelector(t), struct{}{}) { 114 | return 115 | } 116 | } 117 | } 118 | } 119 | 120 | // ToMap return a map that is keyed keySelector and has the value of valSelector for each element in slice. 121 | // If multiple elements return the same key the element that appears later in slice will be chosen. 122 | func ToMap[TSlice ~[]T, T any, K comparable, V any](slice TSlice, keySelector func(T) K, valSelector func(T) V) iter.Seq2[K, V] { 123 | return func(yield func(K, V) bool) { 124 | for _, t := range slice { 125 | if !yield(keySelector(t), valSelector(t)) { 126 | return 127 | } 128 | } 129 | } 130 | } 131 | 132 | // YieldAll iterates over all elements from a provided iterator and returns a slice containing all elements from the iterator. 133 | func YieldAll[TSlice ~[]T, T any](i iter.Seq[T]) TSlice { 134 | result := make(TSlice, 0) 135 | for v := range i { 136 | result = append(result, v) 137 | } 138 | 139 | return result 140 | } 141 | -------------------------------------------------------------------------------- /slices/slice_test.go: -------------------------------------------------------------------------------- 1 | package slices 2 | 3 | import ( 4 | "collections/maps" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestFilter(t *testing.T) { 11 | expected := []int{1, 2, 3} 12 | actual := YieldAll[[]int](Filter([]int{1, 2, 3, 4, 5, 6}, func(x int) bool { return x <= 3 })) 13 | assert.ElementsMatch(t, expected, actual) 14 | expected1 := []string{"test"} 15 | actual1 := YieldAll[[]string](Filter([]string{"asdasd", "123123", "test", "test123123"}, func(x string) bool { return x == "test" })) 16 | assert.ElementsMatch(t, expected1, actual1) 17 | } 18 | 19 | func TestFirst(t *testing.T) { 20 | expected := 1 21 | actual := FirstMatch([]int{-3, -2, -1, 0, 1, 2, 3}, func(x int) bool { return x > 0 }) 22 | assert.Equal(t, expected, actual) 23 | expected = 0 24 | actual = FirstMatch([]int{1, 2, 3}, func(x int) bool { return x < 0 }) 25 | assert.Equal(t, expected, actual) 26 | } 27 | 28 | func TestLast(t *testing.T) { 29 | expected := 3 30 | actual := LastMatch([]int{-3, -2, -1, 0, 1, 2, 3}, func(x int) bool { return x > 0 }) 31 | assert.Equal(t, expected, actual) 32 | expected = 0 33 | actual = LastMatch([]int{1, 2, 3}, func(x int) bool { return x < 0 }) 34 | assert.Equal(t, expected, actual) 35 | } 36 | 37 | func TestAny(t *testing.T) { 38 | assert.True(t, AnyMatch([]int{1, 1, 2, 3, 5, 8, 13}, func(x int) bool { return x > 10 })) 39 | assert.False(t, AnyMatch([]int{1, 1, 2, 3, 5, 8, 13}, func(x int) bool { return x < 0 })) 40 | } 41 | 42 | func TestAll(t *testing.T) { 43 | assert.True(t, AllMatch([]int{1, 2, 3}, func(x int) bool { return x > 0 })) 44 | assert.False(t, AllMatch([]int{-1, 0, 1, 2, 3}, func(x int) bool { return x > 0 })) 45 | } 46 | 47 | func TestMap(t *testing.T) { 48 | assert.ElementsMatch(t, 49 | []int{2, 4, 6, 8}, 50 | Map[[]int]([]int{1, 2, 3, 4}, func(x int) int { return 2 * x }), 51 | ) 52 | assert.ElementsMatch(t, 53 | []int{4, 1, 6}, 54 | Map[[]string]([]string{"test", "a", "golang"}, func(x string) int { return len(x) }), 55 | ) 56 | } 57 | 58 | func TestIndexOf(t *testing.T) { 59 | assert.Equal(t, 2, IndexOf([]int{1, 2, 3, 4, 5}, 3)) 60 | assert.Equal(t, -1, IndexOf([]int{1, 2, 3, 4, 5}, 6)) 61 | } 62 | 63 | func TestContains(t *testing.T) { 64 | assert.True(t, Contains([]int{1, 2, 3, 4}, 3)) 65 | assert.False(t, Contains([]string{"a", "b", "test"}, "wednesday")) 66 | } 67 | 68 | type person struct { 69 | ID int 70 | Name string 71 | ManagerID int 72 | } 73 | 74 | func TestGroupBy(t *testing.T) { 75 | alice := person{1, "Alice", 0} 76 | bob := person{2, "Bob", 1} 77 | carol := person{9, "Carol", 1} 78 | david := person{11, "David", 9} 79 | eugene := person{13, "Eugene", 2} 80 | francene := person{12, "Francene", 9} 81 | employees := []person{alice, bob, carol, david, eugene, francene} 82 | 83 | assert.Equal(t, 84 | map[int][]person{ 85 | 0: {alice}, 86 | 1: {bob, carol}, 87 | 2: {eugene}, 88 | 9: {david, francene}, 89 | }, 90 | GroupBy(employees, func(p person) int { return p.ManagerID }, func(p person) person { return p }), 91 | ) 92 | } 93 | 94 | func TestToSet(t *testing.T) { 95 | people := []person{ 96 | { 97 | ID: 1, 98 | Name: "Alice", 99 | }, 100 | { 101 | ID: 2, 102 | Name: "Bob", 103 | }, 104 | { 105 | ID: 9, 106 | Name: "Carol", 107 | }, 108 | } 109 | 110 | assert.Equal(t, 111 | map[int]struct{}{1: {}, 2: {}, 9: {}}, 112 | maps.YieldAll[map[int]struct{}](ToSet(people, func(p person) int { return p.ID })), 113 | ) 114 | 115 | assert.Equal(t, 116 | map[string]struct{}{ 117 | "Alice": {}, 118 | "Bob": {}, 119 | "Carol": {}, 120 | }, 121 | maps.YieldAll[map[string]struct{}](ToSet(people, func(p person) string { return p.Name })), 122 | ) 123 | } 124 | 125 | func TestToMap(t *testing.T) { 126 | alice := person{1, "Alice", 0} 127 | bob := person{2, "Bob", 1} 128 | carol := person{9, "Carol", 1} 129 | people := []person{alice, bob, carol} 130 | 131 | assert.Equal(t, 132 | map[int]person{ 133 | 1: alice, 134 | 2: bob, 135 | 9: carol, 136 | }, 137 | maps.YieldAll[map[int]person](ToMap(people, func(p person) int { return p.ID }, func(p person) person { return p })), 138 | ) 139 | 140 | assert.Equal(t, 141 | map[string]person{ 142 | "Alice": alice, 143 | "Bob": bob, 144 | "Carol": carol, 145 | }, 146 | maps.YieldAll[map[string]person](ToMap(people, func(p person) string { return p.Name }, func(p person) person { return p })), 147 | ) 148 | } 149 | --------------------------------------------------------------------------------