Skip to content

Commit

Permalink
Rewrite collections package as generic functions (#79)
Browse files Browse the repository at this point in the history
This change also bumps the Go version to 1.18.
  • Loading branch information
infraredgirl authored Nov 11, 2022
1 parent 027003c commit 9082ced
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 180 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
defaults: &defaults
docker:
- image: 087285199408.dkr.ecr.us-east-1.amazonaws.com/circle-ci-test-image-base:go1.17-tf1.2-tg37.4-pck1.8-ci50.1
- image: 087285199408.dkr.ecr.us-east-1.amazonaws.com/circle-ci-test-image-base:go1.18-tf1.3-tg39.1-pck1.8-ci50.7
environment:
GO111MODULE: auto
version: 2.1
Expand Down
22 changes: 11 additions & 11 deletions collections/lists.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package collections

// Return true if the given list contains the given element
func ListContainsElement(list []string, element string) bool {
// ListContainsElement returns true if the given list contains the given element
func ListContainsElement[S ~[]E, E comparable](list S, element any) bool {
for _, item := range list {
if item == element {
return true
Expand All @@ -11,9 +11,9 @@ func ListContainsElement(list []string, element string) bool {
return false
}

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

// MakeCopyOfList will return a new list that is a copy of the given list.
func MakeCopyOfList(list []string) []string {
copyOfList := make([]string, len(list))
func MakeCopyOfList[S ~[]E, E comparable](list S) S {
copyOfList := make(S, len(list))
copy(copyOfList, list)
return copyOfList
}

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

// Taken from SliceTricks: https://github.com/golang/go/wiki/SliceTricks#batching-with-minimal-allocation
// Intuition: We repeatedly slice off batchSize elements from slice and append it to the output, until there
// is not enough.
output := [][]string{}
output := []S{}
for batchSize < len(slice) {
slice, output = slice[batchSize:], append(output, slice[0:batchSize:batchSize])
}
Expand Down
147 changes: 121 additions & 26 deletions collections/lists_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,25 @@ package collections

import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"

"github.com/stretchr/testify/assert"
)

func TestMakeCopyOfListMakesACopy(t *testing.T) {
original := []string{"foo", "bar", "baz"}
copyOfList := MakeCopyOfList(original)
assert.Equal(t, original, copyOfList)
func TestMakeCopyOfList(t *testing.T) {
originalStr := []string{"foo", "bar", "baz"}
copyOfListStr := MakeCopyOfList(originalStr)
assert.Equal(t, originalStr, copyOfListStr)

originalInt := []int{1, 2, 3}
copyOfListInt := MakeCopyOfList(originalInt)
assert.Equal(t, originalInt, copyOfListInt)
}

func TestListContainsElement(t *testing.T) {
t.Parallel()

testCases := []struct {
testCasesStr := []struct {
list []string
element string
expected bool
Expand All @@ -28,7 +33,25 @@ func TestListContainsElement(t *testing.T) {
{[]string{"bar", "foo", "baz"}, "", false},
}

for _, testCase := range testCases {
for _, testCase := range testCasesStr {
actual := ListContainsElement(testCase.list, testCase.element)
assert.Equal(t, testCase.expected, actual, "For list %v and element %s", testCase.list, testCase.element)
}

testCasesInt := []struct {
list []int
element int
expected bool
}{
{[]int{}, 0, false},
{[]int{}, 1, false},
{[]int{1}, 1, true},
{[]int{1, 2, 3}, 1, true},
{[]int{1, 2, 3}, 4, false},
{[]int{1, 2, 3}, 0, false},
}

for _, testCase := range testCasesInt {
actual := ListContainsElement(testCase.list, testCase.element)
assert.Equal(t, testCase.expected, actual, "For list %v and element %s", testCase.list, testCase.element)
}
Expand All @@ -37,7 +60,7 @@ func TestListContainsElement(t *testing.T) {
func TestRemoveElementFromList(t *testing.T) {
t.Parallel()

testCases := []struct {
testCasesStr := []struct {
list []string
element string
expected []string
Expand All @@ -51,7 +74,28 @@ func TestRemoveElementFromList(t *testing.T) {
{[]string{"bar", "foo", "baz"}, "", []string{"bar", "foo", "baz"}},
}

for _, testCase := range testCases {
for _, testCase := range testCasesStr {
actual := RemoveElementFromList(testCase.list, testCase.element)
assert.Equal(t, testCase.expected, actual, "For list %v and element %s", testCase.list, testCase.element)
}

type customInt int

testCasesCustomInt := []struct {
list []customInt
element customInt
expected []customInt
}{
{[]customInt{}, 0, []customInt{}},
{[]customInt{}, 1, []customInt{}},
{[]customInt{1}, 1, []customInt{}},
{[]customInt{1}, 2, []customInt{1}},
{[]customInt{1, 2, 3}, 1, []customInt{2, 3}},
{[]customInt{1, 2, 3}, 4, []customInt{1, 2, 3}},
{[]customInt{1, 2, 3}, 0, []customInt{1, 2, 3}},
}

for _, testCase := range testCasesCustomInt {
actual := RemoveElementFromList(testCase.list, testCase.element)
assert.Equal(t, testCase.expected, actual, "For list %v and element %s", testCase.list, testCase.element)
}
Expand All @@ -60,7 +104,7 @@ func TestRemoveElementFromList(t *testing.T) {
func TestBatchListIntoGroupsOf(t *testing.T) {
t.Parallel()

testCases := []struct {
testCasesStr := []struct {
stringList []string
n int
result [][]string
Expand All @@ -69,33 +113,26 @@ func TestBatchListIntoGroupsOf(t *testing.T) {
[]string{"macaroni", "gentoo", "magellanic", "adelie", "little", "king", "emperor"},
2,
[][]string{
[]string{"macaroni", "gentoo"},
[]string{"magellanic", "adelie"},
[]string{"little", "king"},
[]string{"emperor"},
{"macaroni", "gentoo"},
{"magellanic", "adelie"},
{"little", "king"},
{"emperor"},
},
},
{
[]string{"macaroni", "gentoo", "magellanic", "adelie", "king", "emperor"},
2,
[][]string{
[]string{"macaroni", "gentoo"},
[]string{"magellanic", "adelie"},
[]string{"king", "emperor"},
},
},
{
[]string{"macaroni", "gentoo", "magellanic"},
5,
[][]string{
[]string{"macaroni", "gentoo", "magellanic"},
{"macaroni", "gentoo"},
{"magellanic", "adelie"},
{"king", "emperor"},
},
},
{
[]string{"macaroni", "gentoo", "magellanic"},
5,
[][]string{
[]string{"macaroni", "gentoo", "magellanic"},
{"macaroni", "gentoo", "magellanic"},
},
},
{
Expand All @@ -115,7 +152,7 @@ func TestBatchListIntoGroupsOf(t *testing.T) {
},
}

for idx, testCase := range testCases {
for idx, testCase := range testCasesStr {
t.Run(fmt.Sprintf("%s_%d", t.Name(), idx), func(t *testing.T) {
t.Parallel()
original := MakeCopyOfList(testCase.stringList)
Expand All @@ -124,4 +161,62 @@ func TestBatchListIntoGroupsOf(t *testing.T) {
assert.Equal(t, testCase.stringList, original)
})
}

testCasesInt := []struct {
intList []int
n int
result [][]int
}{
{
[]int{1, 2, 3, 4, 5, 6, 7},
2,
[][]int{
{1, 2},
{3, 4},
{5, 6},
{7},
},
},
{
[]int{1, 2, 3, 4, 5, 6},
2,
[][]int{
{1, 2},
{3, 4},
{5, 6},
},
},
{
[]int{1, 2, 3},
5,
[][]int{
{1, 2, 3},
},
},
{
[]int{1, 2, 3},
-1,
nil,
},
{
[]int{1, 2, 3},
0,
nil,
},
{
[]int{},
7,
[][]int{},
},
}

for idx, testCase := range testCasesInt {
t.Run(fmt.Sprintf("%s_%d", t.Name(), idx), func(t *testing.T) {
t.Parallel()
original := MakeCopyOfList(testCase.intList)
assert.Equal(t, BatchListIntoGroupsOf(testCase.intList, testCase.n), testCase.result)
// Make sure the function doesn't modify the original list
assert.Equal(t, testCase.intList, original)
})
}
}
31 changes: 17 additions & 14 deletions collections/maps.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,37 @@ import (
"fmt"
"sort"
"strings"

"golang.org/x/exp/constraints"
"golang.org/x/exp/maps"
)

const (
DefaultKeyValueStringSliceFormat = "%s=%s"
)

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

for _, currMap := range maps {
for key, value := range currMap {
out[key] = value
}
for _, currMap := range mapsToMerge {
maps.Copy(out, currMap)
}

return out
}

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

for key, _ := range m {
for key := range m {
out = append(out, key)
}

sort.Strings(out)
sort.Slice(out, func(i, j int) bool {
return out[i] < out[j]
})

return out
}
Expand All @@ -42,8 +45,8 @@ func KeyValueStringSlice(m map[string]string) []string {
}

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

for key, value := range m {
Expand Down
Loading

0 comments on commit 9082ced

Please sign in to comment.