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/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) +} diff --git a/join.go b/join.go new file mode 100644 index 0000000..c681015 --- /dev/null +++ b/join.go @@ -0,0 +1,38 @@ +package underscore + +// Joins two slices together and returns a Tuple of [T, []P], the selectors allow you to pick the +// 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, + 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 []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)) + } + + return results +} diff --git a/join_test.go b/join_test.go new file mode 100644 index 0000000..1159af5 --- /dev/null +++ b/join_test.go @@ -0,0 +1,45 @@ +package underscore_test + +import ( + "testing" + + u "github.com/rjNemo/underscore" + "github.com/stretchr/testify/assert" +) + +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"} + +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}}, + } + + 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 new file mode 100644 index 0000000..8ee7671 --- /dev/null +++ b/orderBy.go @@ -0,0 +1,26 @@ +package underscore + +// 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 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 + + 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) +} 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) + } +}