Skip to content

Rewrite collections package as generic functions #79

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 11, 2022
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 .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