From 3066810f92acee2c57287e8aff4c64ea7a681359 Mon Sep 17 00:00:00 2001 From: Andy Long Date: Fri, 2 Sep 2022 00:12:39 +0100 Subject: [PATCH 1/5] Adding some new funky functions which I find useful Created a Tuple struct as some of the new functions require you to return a new slice with two fields which is the result of the new functions Created the Join, JoinProjection, Range, SumMap, Zip functions, ecah fuction is documented with how it works and had a unit test or maybe more --- .gitignore | 1 + join.go | 39 +++++++++++++++++++++++++++++++ join_test.go | 30 ++++++++++++++++++++++++ range.go | 16 +++++++++++++ range_test.go | 24 +++++++++++++++++++ sum.go | 10 ++++++++ sum_test.go | 18 ++++++++++++++ tuple.go | 6 +++++ zip.go | 20 ++++++++++++++++ zip_test.go | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 229 insertions(+) create mode 100644 join.go create mode 100644 join_test.go create mode 100644 range.go create mode 100644 range_test.go create mode 100644 tuple.go create mode 100644 zip.go create mode 100644 zip_test.go diff --git a/.gitignore b/.gitignore index 0fd6007..804df32 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,4 @@ Temporary Items .apdisk docs/public .trivycache/ +.vscode/launch.json diff --git a/join.go b/join.go new file mode 100644 index 0000000..1b2b60b --- /dev/null +++ b/join.go @@ -0,0 +1,39 @@ +package underscore + +// Joins two slices together and returns a Tuple of [T, []P], the selectors allow you to pick the +// keys from your structure you would like to join the sets together with +func Join[T, P any, S comparable]( + left []T, + right []P, + leftSelector func(T) S, + rightSelector func(P) S) []Tuple[T, []P] { + + var results = make([]Tuple[T, []P], 0, len(left)) + for _, l := range left { + var matches = Filter(right, func(r P) bool { return leftSelector(l) == rightSelector(r) }) + var tuple = Tuple[T, []P]{Left: l, Right: matches} + results = append(results, tuple) + } + + return results +} + +// Joins two slices together and returns a []R where R is defined by the output of your projection +// function +// The selectors allow you to pick the keys from your structure you would like to join the sets +// together with. +// While the projection functions allows you to reformat joined datasets contains in the +// Tuple of [T, []P] into your own structure or type +func JoinProject[T, P, R any, S comparable]( + left []T, + right []P, + leftSelector func(T) S, + rightSelector func(P) S, + projection func(Tuple[T, []P]) R) (results []R) { + + for _, x := range Join(left, right, leftSelector, rightSelector) { + results = append(results, projection(x)) + } + + return results +} diff --git a/join_test.go b/join_test.go new file mode 100644 index 0000000..fbf0d4d --- /dev/null +++ b/join_test.go @@ -0,0 +1,30 @@ +package underscore_test + +import ( + "reflect" + "testing" + + u "github.com/rjNemo/underscore" +) + +func Test_Join_Can_Join_Two_Slices_Together(t *testing.T) { + one := u.Tuple[int, string]{Left: 1, Right: "One"} + two := u.Tuple[int, string]{Left: 2, Right: "Two"} + three := u.Tuple[int, string]{Left: 3, Right: "Three"} + + var left = []u.Tuple[int, string]{one, two, three} + var right = []u.Tuple[int, string]{one, three, two, three, two, three} + + selector := func(x u.Tuple[int, string]) int { return x.Left } + + var joined = u.Join(left, right, selector, selector) + var want = []u.Tuple[u.Tuple[int, string], []u.Tuple[int, string]]{ + {Left: one, Right: []u.Tuple[int, string]{one}}, + {Left: two, Right: []u.Tuple[int, string]{two, two}}, + {Left: three, Right: []u.Tuple[int, string]{three, three, three}}, + } + + if !reflect.DeepEqual(joined, want) { + t.Errorf("Expected to get %v but we got %v instead", want, joined) + } +} diff --git a/range.go b/range.go new file mode 100644 index 0000000..204c6b8 --- /dev/null +++ b/range.go @@ -0,0 +1,16 @@ +package underscore + +// Creates a sequence of numbers, i.e. u.Range(0, 3) = [0 1 2 3], while u.Range(3, 0) = [3 2 1 0] +func Range(start int, end int) (result []int) { + if start < end { + for i := start; i <= end; i++ { + result = append(result, i) + } + } else { + for i := start; i >= end; i-- { + result = append(result, i) + } + } + + return result +} diff --git a/range_test.go b/range_test.go new file mode 100644 index 0000000..f59ee5c --- /dev/null +++ b/range_test.go @@ -0,0 +1,24 @@ +package underscore_test + +import ( + "reflect" + "testing" + + u "github.com/rjNemo/underscore" +) + +func Test_Range_Creates_Slices(t *testing.T) { + range1 := u.Range(0, 5) + want1 := []int{0, 1, 2, 3, 4, 5} + + if !reflect.DeepEqual(range1, want1) { + t.Errorf("Expected the result to be %v but we got %v", want1, range1) + } + + range2 := u.Range(5, 0) + want2 := []int{5, 4, 3, 2, 1, 0} + + if !reflect.DeepEqual(range2, want2) { + t.Errorf("Expected the result to be %v but we got %v", want2, range2) + } +} diff --git a/sum.go b/sum.go index 3d2b5db..0cc9652 100644 --- a/sum.go +++ b/sum.go @@ -9,3 +9,13 @@ func Sum[T constraints.Ordered](values []T) (sum T) { } return sum } + +// Sums the values you select from your struct, basically a sort cut instead of +// having to perform a u.Map followed by a u.Sum +func SumMap[T any, R constraints.Ordered](list []T, selector func(T) R) (sum R) { + for _, v := range list { + sum += selector(v) + } + + return sum +} diff --git a/sum_test.go b/sum_test.go index 05a2a8a..4d5556d 100644 --- a/sum_test.go +++ b/sum_test.go @@ -13,3 +13,21 @@ func TestSum(t *testing.T) { assert.Equal(t, want, u.Sum(nums)) } + +func TestSumMap(t *testing.T) { + nums := []u.Tuple[string, int]{ + {"zero", 0}, + {"one", 1}, + {"two", 2}, + {"three", 3}, + {"four", 4}, + {"five", 5}, + {"six", 6}, + {"seven", 7}, + {"eight", 8}, + {"nine", 9}, + } + want := 45 + + assert.Equal(t, want, u.SumMap(nums, func(item u.Tuple[string, int]) int { return item.Right })) +} diff --git a/tuple.go b/tuple.go new file mode 100644 index 0000000..a229f52 --- /dev/null +++ b/tuple.go @@ -0,0 +1,6 @@ +package underscore + +type Tuple[L, R any] struct { + Left L + Right R +} diff --git a/zip.go b/zip.go new file mode 100644 index 0000000..ca53e62 --- /dev/null +++ b/zip.go @@ -0,0 +1,20 @@ +package underscore + +// Zips two slices togther so all the elements of left slice are attached to the corresponding +// elements of the right slice, i.e. [one two three] [1 2 3 4] = [{one, 1} {two, 2} {three, 3}] +// the returned data will be the size of the smallest slice +func Zip[L any, R any](left []L, right []R) []Tuple[L, R] { + shortest := 0 + if len(left) < len(right) { + shortest = len(left) + } else { + shortest = len(right) + } + + results := make([]Tuple[L, R], shortest) + for i := 0; i < shortest; i++ { + results[i] = Tuple[L, R]{Left: left[i], Right: right[i]} + } + + return results +} diff --git a/zip_test.go b/zip_test.go new file mode 100644 index 0000000..66b8ddf --- /dev/null +++ b/zip_test.go @@ -0,0 +1,65 @@ +package underscore_test + +import ( + "reflect" + "testing" + + u "github.com/rjNemo/underscore" +) + +func Test_Zip_Can_Zip_Two_Equal_Sized_Slices(t *testing.T) { + left := []string{"Left 1", "Left 2", "Left 3"} + right := []int{1, 2, 3} + + var zipped = u.Zip(left, right) + + want := []u.Tuple[string, int]{ + {Left: "Left 1", Right: 1}, + {Left: "Left 2", Right: 2}, + {Left: "Left 3", Right: 3}, + } + + if !reflect.DeepEqual(zipped, want) { + t.Errorf("Expected the result to be %v but we got %v", want, zipped) + } +} + +func Test_Zip_Can_Zip_Two_Different_Sized_Slices_Left_Larger(t *testing.T) { + left := []string{"Left 1", "Left 2", "Left 3", "Left 4"} + right := []int{1, 2, 3} + + var zipped = u.Zip(left, right) + if len(zipped) != 3 { + t.Errorf("Expected the result of Zip(left, right) to have a length of 3 but got %v", len(zipped)) + } + + want := []u.Tuple[string, int]{ + {Left: "Left 1", Right: 1}, + {Left: "Left 2", Right: 2}, + {Left: "Left 3", Right: 3}, + } + + if !reflect.DeepEqual(zipped, want) { + t.Errorf("Expected the result to be %v but we got %v", want, zipped) + } +} + +func Test_Zip_Can_Zip_Two_Different_Sized_Slices_Right_Larger(t *testing.T) { + left := []string{"Left 1", "Left 2", "Left 3"} + right := []int{1, 2, 3, 4} + + var zipped = u.Zip(left, right) + if len(zipped) != 3 { + t.Errorf("Expected the result of Zip(left, right) to have a length of 3 but got %v", len(zipped)) + } + + want := []u.Tuple[string, int]{ + {Left: "Left 1", Right: 1}, + {Left: "Left 2", Right: 2}, + {Left: "Left 3", Right: 3}, + } + + if !reflect.DeepEqual(zipped, want) { + t.Errorf("Expected the result to be %v but we got %v", want, zipped) + } +} From a197836c1db6fcf3cd2caec927c665ee014972a9 Mon Sep 17 00:00:00 2001 From: Andy Long Date: Sat, 3 Sep 2022 16:04:30 +0100 Subject: [PATCH 2/5] Added in an OrderBy function --- orderBy.go | 22 ++++++++++++++++++++++ orderBy_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 orderBy.go create mode 100644 orderBy_test.go diff --git a/orderBy.go b/orderBy.go new file mode 100644 index 0000000..b61093d --- /dev/null +++ b/orderBy.go @@ -0,0 +1,22 @@ +package underscore + +func OrderBy[T any](list []T, predicate func(T, T) bool) []T { + swaps := true + var tmp T + + for swaps { + swaps = false + + for i := 0; i < len(list)-1; i++ { + if predicate(list[i], list[i+1]) { + swaps = true + tmp = list[i] + + list[i] = list[i+1] + list[i+1] = tmp + } + } + } + + return list +} diff --git a/orderBy_test.go b/orderBy_test.go new file mode 100644 index 0000000..ac0d802 --- /dev/null +++ b/orderBy_test.go @@ -0,0 +1,31 @@ +package underscore_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + u "github.com/rjNemo/underscore" +) + +func Test_OrderBy_Asc(t *testing.T) { + set := u.Range(5, 0) + want := u.Range(0, 5) + + result := u.OrderBy(set, func(left int, right int) bool { + return left > right + }) + + assert.Equal(t, want, result) +} + +func Test_OrderBy_Desc(t *testing.T) { + set := u.Range(0, 5) + want := u.Range(5, 0) + + result := u.OrderBy(set, func(left int, right int) bool { + return left < right + }) + + assert.Equal(t, want, result) +} From a72da82172bde13c5bea095563ad7d6f89348ef9 Mon Sep 17 00:00:00 2001 From: Andy Long Date: Sat, 3 Sep 2022 19:31:41 +0100 Subject: [PATCH 3/5] Documentation comment for OrderBy which I missed out --- orderBy.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/orderBy.go b/orderBy.go index b61093d..36435c5 100644 --- a/orderBy.go +++ b/orderBy.go @@ -1,5 +1,8 @@ package underscore +// Orders a slice by a field value within a struct, the predicate allow you +// to pick the fields you want to orderBy. Use > for ASC or < for DESC +// func (left Person, right Person) bool { return person.Age > person.Age } func OrderBy[T any](list []T, predicate func(T, T) bool) []T { swaps := true var tmp T From 581929a555bfe63cee2575a3dc2c66160a99121e Mon Sep 17 00:00:00 2001 From: Andy Long Date: Sat, 3 Sep 2022 23:23:26 +0100 Subject: [PATCH 4/5] Adding a Unit test for JoinProject function Updated the comments on the Join & OrderBy functions so they make a little more sense. Covered an extra test case with the Join test, where the left set has more data than the right and so the Right handside array of the join is empty --- join.go | 25 ++++++++++++------------- join_test.go | 31 +++++++++++++++++++++++-------- orderBy.go | 5 +++-- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/join.go b/join.go index 1b2b60b..c681015 100644 --- a/join.go +++ b/join.go @@ -1,7 +1,7 @@ package underscore // Joins two slices together and returns a Tuple of [T, []P], the selectors allow you to pick the -// keys from your structure you would like to join the sets together with +// keys you want to use from your struct's to join the sets together func Join[T, P any, S comparable]( left []T, right []P, @@ -18,18 +18,17 @@ func Join[T, P any, S comparable]( return results } -// Joins two slices together and returns a []R where R is defined by the output of your projection -// function -// The selectors allow you to pick the keys from your structure you would like to join the sets -// together with. -// While the projection functions allows you to reformat joined datasets contains in the -// Tuple of [T, []P] into your own structure or type -func JoinProject[T, P, R any, S comparable]( - left []T, - right []P, - leftSelector func(T) S, - rightSelector func(P) S, - projection func(Tuple[T, []P]) R) (results []R) { +// Joins two slices together and returns a []O where O is defined by the output +// of your projection function +// The selectors allow you to pick the keys from your structure to use as the join keys +// While the projection functions allows you to reformat joined datasets +// (Tuple of [T, []P]) into your own struct or type +func JoinProject[L, R, O any, S comparable]( + left []L, + right []R, + leftSelector func(L) S, + rightSelector func(R) S, + projection func(Tuple[L, []R]) O) (results []O) { for _, x := range Join(left, right, leftSelector, rightSelector) { results = append(results, projection(x)) diff --git a/join_test.go b/join_test.go index fbf0d4d..1159af5 100644 --- a/join_test.go +++ b/join_test.go @@ -1,30 +1,45 @@ package underscore_test import ( - "reflect" "testing" u "github.com/rjNemo/underscore" + "github.com/stretchr/testify/assert" ) -func Test_Join_Can_Join_Two_Slices_Together(t *testing.T) { - one := u.Tuple[int, string]{Left: 1, Right: "One"} - two := u.Tuple[int, string]{Left: 2, Right: "Two"} - three := u.Tuple[int, string]{Left: 3, Right: "Three"} +var zero = u.Tuple[int, string]{Left: 0, Right: "Zero"} +var one = u.Tuple[int, string]{Left: 1, Right: "One"} +var two = u.Tuple[int, string]{Left: 2, Right: "Two"} +var three = u.Tuple[int, string]{Left: 3, Right: "Three"} - var left = []u.Tuple[int, string]{one, two, three} +func Test_Join_Can_Join_Two_Slices_Together(t *testing.T) { + var left = []u.Tuple[int, string]{zero, one, two, three} var right = []u.Tuple[int, string]{one, three, two, three, two, three} selector := func(x u.Tuple[int, string]) int { return x.Left } var joined = u.Join(left, right, selector, selector) var want = []u.Tuple[u.Tuple[int, string], []u.Tuple[int, string]]{ + {Left: zero, Right: nil}, {Left: one, Right: []u.Tuple[int, string]{one}}, {Left: two, Right: []u.Tuple[int, string]{two, two}}, {Left: three, Right: []u.Tuple[int, string]{three, three, three}}, } - if !reflect.DeepEqual(joined, want) { - t.Errorf("Expected to get %v but we got %v instead", want, joined) + assert.Equal(t, want, joined) +} + +func Test_Join_Can_Join_and_Project_Two_Slices_Together(t *testing.T) { + var left = []u.Tuple[int, string]{zero, one, two, three} + var right = []u.Tuple[int, string]{one, three, two, three, two, three} + + selector := func(x u.Tuple[int, string]) int { return x.Left } + project := func(x u.Tuple[u.Tuple[int, string], []u.Tuple[int, string]]) int { + return len(x.Right) // projecting to a could of how many } + + var joined = u.JoinProject(left, right, selector, selector, project) + var want = []int{0, 1, 2, 3} + + assert.Equal(t, want, joined) } diff --git a/orderBy.go b/orderBy.go index 36435c5..8ee7671 100644 --- a/orderBy.go +++ b/orderBy.go @@ -1,12 +1,13 @@ package underscore -// Orders a slice by a field value within a struct, the predicate allow you +// Orders a slice by a field value within a struct, the predicate allows you // to pick the fields you want to orderBy. Use > for ASC or < for DESC -// func (left Person, right Person) bool { return person.Age > person.Age } +// func (left Person, right Person) bool { return left.Age > right.Age } func OrderBy[T any](list []T, predicate func(T, T) bool) []T { swaps := true var tmp T + //todo: replace with a faster algorithm, this one is pretty simple for swaps { swaps = false From aee22a45bc8028c4ef52e0fb6f4db6e28cf75875 Mon Sep 17 00:00:00 2001 From: Andy Long Date: Wed, 30 Nov 2022 11:23:56 +0000 Subject: [PATCH 5/5] Adding a count method to the package, so you can find out how many items in a slice satisfy and given condition --- count.go | 13 +++++++++++ count_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 count.go create mode 100644 count_test.go diff --git a/count.go b/count.go new file mode 100644 index 0000000..581c053 --- /dev/null +++ b/count.go @@ -0,0 +1,13 @@ +package underscore + +// Count returns the number of elements in the slice that satisfy the predicate. +// example: Count([]int{1,2,3,4,5}, func(n int) bool { return n%2 == 0 }) // 2 +func Count[T comparable](slice []T, predicate func(T) bool) int { + count := 0 + for _, item := range slice { + if predicate(item) { + count++ + } + } + return count +} diff --git a/count_test.go b/count_test.go new file mode 100644 index 0000000..9a876ef --- /dev/null +++ b/count_test.go @@ -0,0 +1,64 @@ +package underscore + +import ( + "github.com/stretchr/testify/assert" + "strings" + "testing" +) + +func Test_Count_Can_Count_Numbers(t *testing.T) { + numbers := Range(1, 100) + count := Count(numbers, func(n int) bool { + return n%2 == 0 + }) + + assert.Equal(t, 50, count) +} + +type People struct { + Name string + Age int + Gender string +} + +func Test_Count_Can_Count__People(t *testing.T) { + people := []People{ + {Name: "Andy", Age: 43, Gender: "M"}, + {Name: "Fred", Age: 33, Gender: "M"}, + {Name: "Jack", Age: 23, Gender: "M"}, + {Name: "Jill", Age: 43, Gender: "F"}, + {Name: "Anna", Age: 33, Gender: "F"}, + {Name: "Arya", Age: 23, Gender: "F"}, + {Name: "Jane", Age: 13, Gender: "F"}, + } + + a := Count(people, func(p People) bool { + return strings.HasPrefix(p.Name, "A") + }) + assert.Equal(t, 3, a) + + females := Count(people, func(p People) bool { + return p.Gender == "F" + }) + assert.Equal(t, 4, females) + + males := Count(people, func(p People) bool { + return p.Gender == "M" + }) + assert.Equal(t, 3, males) + + over30 := Count(people, func(p People) bool { + return p.Age > 30 + }) + assert.Equal(t, 4, over30) + + under30 := Count(people, func(p People) bool { + return p.Age < 30 + }) + assert.Equal(t, 3, under30) + + under20 := Count(people, func(p People) bool { + return p.Age < 20 + }) + assert.Equal(t, 1, under20) +}