Skip to content

Commit 9082ced

Browse files
authored
Rewrite collections package as generic functions (#79)
This change also bumps the Go version to 1.18.
1 parent 027003c commit 9082ced

File tree

7 files changed

+338
-180
lines changed

7 files changed

+338
-180
lines changed

.circleci/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
defaults: &defaults
22
docker:
3-
- image: 087285199408.dkr.ecr.us-east-1.amazonaws.com/circle-ci-test-image-base:go1.17-tf1.2-tg37.4-pck1.8-ci50.1
3+
- image: 087285199408.dkr.ecr.us-east-1.amazonaws.com/circle-ci-test-image-base:go1.18-tf1.3-tg39.1-pck1.8-ci50.7
44
environment:
55
GO111MODULE: auto
66
version: 2.1

collections/lists.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package collections
22

3-
// Return true if the given list contains the given element
4-
func ListContainsElement(list []string, element string) bool {
3+
// ListContainsElement returns true if the given list contains the given element
4+
func ListContainsElement[S ~[]E, E comparable](list S, element any) bool {
55
for _, item := range list {
66
if item == element {
77
return true
@@ -11,9 +11,9 @@ func ListContainsElement(list []string, element string) bool {
1111
return false
1212
}
1313

14-
// Return a copy of the given list with all instances of the given element removed
15-
func RemoveElementFromList(list []string, element string) []string {
16-
out := []string{}
14+
// RemoveElementFromList returns a copy of the given list with all instances of the given element removed
15+
func RemoveElementFromList[S ~[]E, E comparable](list S, element any) S {
16+
out := S{}
1717
for _, item := range list {
1818
if item != element {
1919
out = append(out, item)
@@ -23,23 +23,23 @@ func RemoveElementFromList(list []string, element string) []string {
2323
}
2424

2525
// MakeCopyOfList will return a new list that is a copy of the given list.
26-
func MakeCopyOfList(list []string) []string {
27-
copyOfList := make([]string, len(list))
26+
func MakeCopyOfList[S ~[]E, E comparable](list S) S {
27+
copyOfList := make(S, len(list))
2828
copy(copyOfList, list)
2929
return copyOfList
3030
}
3131

32-
// BatchListIntoGroupsOf will group the provided string slice into groups of size n, with the last of being truncated to
33-
// the remaining count of strings. Returns nil if n is <= 0
34-
func BatchListIntoGroupsOf(slice []string, batchSize int) [][]string {
32+
// BatchListIntoGroupsOf will group the provided slice into groups of size n, with the last of being truncated to
33+
// the remaining count of elements. Returns nil if n is <= 0
34+
func BatchListIntoGroupsOf[S ~[]E, E comparable](slice S, batchSize int) []S {
3535
if batchSize <= 0 {
3636
return nil
3737
}
3838

3939
// Taken from SliceTricks: https://github.com/golang/go/wiki/SliceTricks#batching-with-minimal-allocation
4040
// Intuition: We repeatedly slice off batchSize elements from slice and append it to the output, until there
4141
// is not enough.
42-
output := [][]string{}
42+
output := []S{}
4343
for batchSize < len(slice) {
4444
slice, output = slice[batchSize:], append(output, slice[0:batchSize:batchSize])
4545
}

collections/lists_test.go

Lines changed: 121 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,25 @@ package collections
22

33
import (
44
"fmt"
5-
"github.com/stretchr/testify/assert"
65
"testing"
6+
7+
"github.com/stretchr/testify/assert"
78
)
89

9-
func TestMakeCopyOfListMakesACopy(t *testing.T) {
10-
original := []string{"foo", "bar", "baz"}
11-
copyOfList := MakeCopyOfList(original)
12-
assert.Equal(t, original, copyOfList)
10+
func TestMakeCopyOfList(t *testing.T) {
11+
originalStr := []string{"foo", "bar", "baz"}
12+
copyOfListStr := MakeCopyOfList(originalStr)
13+
assert.Equal(t, originalStr, copyOfListStr)
14+
15+
originalInt := []int{1, 2, 3}
16+
copyOfListInt := MakeCopyOfList(originalInt)
17+
assert.Equal(t, originalInt, copyOfListInt)
1318
}
1419

1520
func TestListContainsElement(t *testing.T) {
1621
t.Parallel()
1722

18-
testCases := []struct {
23+
testCasesStr := []struct {
1924
list []string
2025
element string
2126
expected bool
@@ -28,7 +33,25 @@ func TestListContainsElement(t *testing.T) {
2833
{[]string{"bar", "foo", "baz"}, "", false},
2934
}
3035

31-
for _, testCase := range testCases {
36+
for _, testCase := range testCasesStr {
37+
actual := ListContainsElement(testCase.list, testCase.element)
38+
assert.Equal(t, testCase.expected, actual, "For list %v and element %s", testCase.list, testCase.element)
39+
}
40+
41+
testCasesInt := []struct {
42+
list []int
43+
element int
44+
expected bool
45+
}{
46+
{[]int{}, 0, false},
47+
{[]int{}, 1, false},
48+
{[]int{1}, 1, true},
49+
{[]int{1, 2, 3}, 1, true},
50+
{[]int{1, 2, 3}, 4, false},
51+
{[]int{1, 2, 3}, 0, false},
52+
}
53+
54+
for _, testCase := range testCasesInt {
3255
actual := ListContainsElement(testCase.list, testCase.element)
3356
assert.Equal(t, testCase.expected, actual, "For list %v and element %s", testCase.list, testCase.element)
3457
}
@@ -37,7 +60,7 @@ func TestListContainsElement(t *testing.T) {
3760
func TestRemoveElementFromList(t *testing.T) {
3861
t.Parallel()
3962

40-
testCases := []struct {
63+
testCasesStr := []struct {
4164
list []string
4265
element string
4366
expected []string
@@ -51,7 +74,28 @@ func TestRemoveElementFromList(t *testing.T) {
5174
{[]string{"bar", "foo", "baz"}, "", []string{"bar", "foo", "baz"}},
5275
}
5376

54-
for _, testCase := range testCases {
77+
for _, testCase := range testCasesStr {
78+
actual := RemoveElementFromList(testCase.list, testCase.element)
79+
assert.Equal(t, testCase.expected, actual, "For list %v and element %s", testCase.list, testCase.element)
80+
}
81+
82+
type customInt int
83+
84+
testCasesCustomInt := []struct {
85+
list []customInt
86+
element customInt
87+
expected []customInt
88+
}{
89+
{[]customInt{}, 0, []customInt{}},
90+
{[]customInt{}, 1, []customInt{}},
91+
{[]customInt{1}, 1, []customInt{}},
92+
{[]customInt{1}, 2, []customInt{1}},
93+
{[]customInt{1, 2, 3}, 1, []customInt{2, 3}},
94+
{[]customInt{1, 2, 3}, 4, []customInt{1, 2, 3}},
95+
{[]customInt{1, 2, 3}, 0, []customInt{1, 2, 3}},
96+
}
97+
98+
for _, testCase := range testCasesCustomInt {
5599
actual := RemoveElementFromList(testCase.list, testCase.element)
56100
assert.Equal(t, testCase.expected, actual, "For list %v and element %s", testCase.list, testCase.element)
57101
}
@@ -60,7 +104,7 @@ func TestRemoveElementFromList(t *testing.T) {
60104
func TestBatchListIntoGroupsOf(t *testing.T) {
61105
t.Parallel()
62106

63-
testCases := []struct {
107+
testCasesStr := []struct {
64108
stringList []string
65109
n int
66110
result [][]string
@@ -69,33 +113,26 @@ func TestBatchListIntoGroupsOf(t *testing.T) {
69113
[]string{"macaroni", "gentoo", "magellanic", "adelie", "little", "king", "emperor"},
70114
2,
71115
[][]string{
72-
[]string{"macaroni", "gentoo"},
73-
[]string{"magellanic", "adelie"},
74-
[]string{"little", "king"},
75-
[]string{"emperor"},
116+
{"macaroni", "gentoo"},
117+
{"magellanic", "adelie"},
118+
{"little", "king"},
119+
{"emperor"},
76120
},
77121
},
78122
{
79123
[]string{"macaroni", "gentoo", "magellanic", "adelie", "king", "emperor"},
80124
2,
81125
[][]string{
82-
[]string{"macaroni", "gentoo"},
83-
[]string{"magellanic", "adelie"},
84-
[]string{"king", "emperor"},
85-
},
86-
},
87-
{
88-
[]string{"macaroni", "gentoo", "magellanic"},
89-
5,
90-
[][]string{
91-
[]string{"macaroni", "gentoo", "magellanic"},
126+
{"macaroni", "gentoo"},
127+
{"magellanic", "adelie"},
128+
{"king", "emperor"},
92129
},
93130
},
94131
{
95132
[]string{"macaroni", "gentoo", "magellanic"},
96133
5,
97134
[][]string{
98-
[]string{"macaroni", "gentoo", "magellanic"},
135+
{"macaroni", "gentoo", "magellanic"},
99136
},
100137
},
101138
{
@@ -115,7 +152,7 @@ func TestBatchListIntoGroupsOf(t *testing.T) {
115152
},
116153
}
117154

118-
for idx, testCase := range testCases {
155+
for idx, testCase := range testCasesStr {
119156
t.Run(fmt.Sprintf("%s_%d", t.Name(), idx), func(t *testing.T) {
120157
t.Parallel()
121158
original := MakeCopyOfList(testCase.stringList)
@@ -124,4 +161,62 @@ func TestBatchListIntoGroupsOf(t *testing.T) {
124161
assert.Equal(t, testCase.stringList, original)
125162
})
126163
}
164+
165+
testCasesInt := []struct {
166+
intList []int
167+
n int
168+
result [][]int
169+
}{
170+
{
171+
[]int{1, 2, 3, 4, 5, 6, 7},
172+
2,
173+
[][]int{
174+
{1, 2},
175+
{3, 4},
176+
{5, 6},
177+
{7},
178+
},
179+
},
180+
{
181+
[]int{1, 2, 3, 4, 5, 6},
182+
2,
183+
[][]int{
184+
{1, 2},
185+
{3, 4},
186+
{5, 6},
187+
},
188+
},
189+
{
190+
[]int{1, 2, 3},
191+
5,
192+
[][]int{
193+
{1, 2, 3},
194+
},
195+
},
196+
{
197+
[]int{1, 2, 3},
198+
-1,
199+
nil,
200+
},
201+
{
202+
[]int{1, 2, 3},
203+
0,
204+
nil,
205+
},
206+
{
207+
[]int{},
208+
7,
209+
[][]int{},
210+
},
211+
}
212+
213+
for idx, testCase := range testCasesInt {
214+
t.Run(fmt.Sprintf("%s_%d", t.Name(), idx), func(t *testing.T) {
215+
t.Parallel()
216+
original := MakeCopyOfList(testCase.intList)
217+
assert.Equal(t, BatchListIntoGroupsOf(testCase.intList, testCase.n), testCase.result)
218+
// Make sure the function doesn't modify the original list
219+
assert.Equal(t, testCase.intList, original)
220+
})
221+
}
127222
}

collections/maps.go

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,37 @@ import (
44
"fmt"
55
"sort"
66
"strings"
7+
8+
"golang.org/x/exp/constraints"
9+
"golang.org/x/exp/maps"
710
)
811

912
const (
1013
DefaultKeyValueStringSliceFormat = "%s=%s"
1114
)
1215

13-
// Merge all the maps into one. Sadly, Go has no generics, so this is only defined for string to interface maps.
14-
func MergeMaps(maps ...map[string]interface{}) map[string]interface{} {
15-
out := map[string]interface{}{}
16+
// MergeMaps merges all the maps into one
17+
func MergeMaps[K comparable, V any](mapsToMerge ...map[K]V) map[K]V {
18+
out := map[K]V{}
1619

17-
for _, currMap := range maps {
18-
for key, value := range currMap {
19-
out[key] = value
20-
}
20+
for _, currMap := range mapsToMerge {
21+
maps.Copy(out, currMap)
2122
}
2223

2324
return out
2425
}
2526

26-
// Return the keys for the given map, sorted alphabetically
27-
func Keys(m map[string]string) []string {
28-
out := []string{}
27+
// Keys returns the keys for the given map, sorted
28+
func Keys[K constraints.Ordered, V any](m map[K]V) []K {
29+
out := []K{}
2930

30-
for key, _ := range m {
31+
for key := range m {
3132
out = append(out, key)
3233
}
3334

34-
sort.Strings(out)
35+
sort.Slice(out, func(i, j int) bool {
36+
return out[i] < out[j]
37+
})
3538

3639
return out
3740
}
@@ -42,8 +45,8 @@ func KeyValueStringSlice(m map[string]string) []string {
4245
}
4346

4447
// KeyValueStringSliceWithFormat returns a string slice using the specified format, sorted alphabetically.
45-
// The format should consist of at least two '%s' string verbs.
46-
func KeyValueStringSliceWithFormat(m map[string]string, format string) []string {
48+
// The format should consist of at least two format specifiers.
49+
func KeyValueStringSliceWithFormat[K comparable, V any](m map[K]V, format string) []string {
4750
out := []string{}
4851

4952
for key, value := range m {

0 commit comments

Comments
 (0)