From d166a38484e439ff61456ad16b6b5e5388ae3a40 Mon Sep 17 00:00:00 2001 From: Ann Date: Mon, 14 Oct 2019 15:53:21 +0700 Subject: [PATCH 1/2] Add GroupBy function and example run --- groupBy.go | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ groupBy_test.go | 45 ++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 groupBy.go create mode 100644 groupBy_test.go diff --git a/groupBy.go b/groupBy.go new file mode 100644 index 0000000..f431544 --- /dev/null +++ b/groupBy.go @@ -0,0 +1,99 @@ +package godash + +import ( + "fmt" + "reflect" +) + +// GroupBy creates an object composed of keys generated from the results of running +// each element of slice throught iteration. The order of grouped values +// is determined by the order they occur in `collection`. The corresponding +// value of each key is an array of elements responsible for generating the +// key. +// +// Validations: +// +// 1. Group function should take one argument and return one value +// 2. Group function should return a one value +// 3. Group function's argument should be of the same type as the elements of the input slice +// 4. Output should be a map +// 5. The key type's output should be of the same type as the predicate function output +// 6. The value type's output (as sliceValueOutput) should be a slice +// 7. The element type of the sliceValueOutput should be of the same type as the element type of the input slice +// Validation errors are returned to the caller +func GroupBy(in, out, groupFn interface{}) error { + + input := reflect.ValueOf(in) + output := reflect.ValueOf(out) + group := reflect.ValueOf(groupFn) + + if group.Kind() != reflect.Func { + return fmt.Errorf("groupFn (%s) has to be a function", group.Kind()) + } + + groupFnType := group.Type() + + if groupFnType.NumIn() != 1 { + return fmt.Errorf("group function has to take only one argument") + } + + if groupFnType.NumOut() != 1 { + return fmt.Errorf("group function should return only one return value") + } + + outputType := output.Elem().Type() + outputKind := output.Elem().Kind() + + if outputKind != reflect.Map { + return fmt.Errorf("output has to be a map") + } + + if groupFnType.Out(0).Kind() != outputType.Key().Kind() { + return fmt.Errorf("group function should return the type of key's output") + } + + outputSliceType := outputType.Elem() + outputSliceKind := outputSliceType.Kind() + + if outputSliceKind != reflect.Slice { + return fmt.Errorf("The type of value's output should be a slice") + } + + inputKind := input.Kind() + + if inputKind == reflect.Slice { + inputSliceElemType := input.Type().Elem() + groupFnArgType := groupFnType.In(0) + + if inputSliceElemType != outputSliceType.Elem() { + return fmt.Errorf("The type of element of value's slice has to be a same the type of input") + } + + if inputSliceElemType != groupFnArgType { + return fmt.Errorf("group function's argument (%s) has to be (%s)", groupFnArgType, inputSliceElemType) + } + + result := reflect.MakeMap(outputType) + + for i := 0; i < input.Len(); i++ { + arg := input.Index(i) + argValues := []reflect.Value{arg} + returnValue := group.Call(argValues)[0] + slice := result.MapIndex(returnValue) + + if slice.IsValid() { + slice = reflect.Append(slice, argValues[0]) + result.SetMapIndex(returnValue, slice) + + } else { + slice := reflect.MakeSlice(outputSliceType, 0, input.Len()) + slice = reflect.Append(slice, argValues[0]) + result.SetMapIndex(returnValue, slice) + } + } + output.Elem().Set(result) + + return nil + } + return fmt.Errorf("not implemented for (%s)", inputKind) +} diff --git a/groupBy_test.go b/groupBy_test.go new file mode 100644 index 0000000..7f9f6a4 --- /dev/null +++ b/groupBy_test.go @@ -0,0 +1,45 @@ +package godash_test + +import ( + "fmt" + "testing" + "github.com/thecasualcoder/godash" +) + +func TestGroupBy(t *testing.T) { + +} + +func ExampleGroupBy() { + type Person struct { + name string + age int + } + john := Person{name: "John", age: 25} + doe := Person{name: "Doe", age: 30} + wick := Person{name: "Wick", age: 25} + + input := []Person{ + john, + doe, + wick, + } + + var output map[int][]Person + + godash.GroupBy(input, &output, func(person Person) int { + return person.age + }) + fmt.Printf("Groups Count: %d", len(output)) + for k, v := range output { + fmt.Printf("\nGroup %d: ", k) + for _, elem := range v { + fmt.Printf(" %s,", elem.name) + } + } + + // Output: + // Groups Count: 2 + // Group 25: John, Wick, + // Group 30: Doe, +} From ae69bdc105a211c8548084dd812c8241fdb3f717 Mon Sep 17 00:00:00 2001 From: Ann Date: Mon, 14 Oct 2019 20:23:24 +0700 Subject: [PATCH 2/2] Add more test for GroupBy and update readme --- README.md | 36 +++++++++++++++ groupBy.go | 13 +++--- groupBy_test.go | 117 ++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 155 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 8f80d05..96b5251 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ This library heavily makes use of `reflect` package and hence will have an **imp 2. [Filter](#Filter) 3. [Reduce](#Reduce) +4. [GroupBy](#GroupBy) + ## Usages ### Map @@ -140,3 +142,37 @@ func main() { fmt.Println(output) // prints 45 } ``` + +### GroupBy + +GroupBy creates an object composed of keys generated from the results of running each element of slice throught iteration. The order of grouped values is determined by the order they occur in slice. The corresponding value of each key is an array of elements responsible for generating the key. +For more [docs](https://godoc.org/github.com/thecasualcoder/godash#GroupBy). + +```go +func main() { + input := []Person{ + Person{name: "John", age: 25}, + Person{name: "Doe", age: 30}, + Person{name: "Wick", age: 25}, + } + + var output map[int][]Person + + godash.GroupBy(input, &output, func(person Person) int { + return person.age + }) + + fmt.Printf("Groups Count: %d", len(output)) + for k, v := range output { + fmt.Printf("\nGroup %d: ", k) + for _, elem := range v { + fmt.Printf(" %s,", elem.name) + } + } + + // Output: + // Groups Count: 2 + // Group 25: John, Wick, + // Group 30: Doe, +} +``` diff --git a/groupBy.go b/groupBy.go index f431544..1aa9dd5 100644 --- a/groupBy.go +++ b/groupBy.go @@ -7,7 +7,7 @@ import ( // GroupBy creates an object composed of keys generated from the results of running // each element of slice throught iteration. The order of grouped values -// is determined by the order they occur in `collection`. The corresponding +// is determined by the order they occur in slice. The corresponding // value of each key is an array of elements responsible for generating the // key. // @@ -28,7 +28,7 @@ func GroupBy(in, out, groupFn interface{}) error { group := reflect.ValueOf(groupFn) if group.Kind() != reflect.Func { - return fmt.Errorf("groupFn (%s) has to be a function", group.Kind()) + return fmt.Errorf("groupFn should to be a function") } groupFnType := group.Type() @@ -78,17 +78,16 @@ func GroupBy(in, out, groupFn interface{}) error { for i := 0; i < input.Len(); i++ { arg := input.Index(i) argValues := []reflect.Value{arg} - returnValue := group.Call(argValues)[0] - slice := result.MapIndex(returnValue) + key := group.Call(argValues)[0] - if slice.IsValid() { + if slice := result.MapIndex(key); slice.IsValid() { slice = reflect.Append(slice, argValues[0]) - result.SetMapIndex(returnValue, slice) + result.SetMapIndex(key, slice) } else { slice := reflect.MakeSlice(outputSliceType, 0, input.Len()) slice = reflect.Append(slice, argValues[0]) - result.SetMapIndex(returnValue, slice) + result.SetMapIndex(key, slice) } } output.Elem().Set(result) diff --git a/groupBy_test.go b/groupBy_test.go index 7f9f6a4..c65a1b8 100644 --- a/groupBy_test.go +++ b/groupBy_test.go @@ -3,18 +3,126 @@ package godash_test import ( "fmt" "testing" + + "github.com/stretchr/testify/assert" "github.com/thecasualcoder/godash" ) +type Person struct { + name string + age int +} + func TestGroupBy(t *testing.T) { + t.Run(fmt.Sprintf("GroupBy should return err if groupFn is not a function"), func(t *testing.T) { + in := []Person{} + var output map[int][]Person + + err := godash.GroupBy(in, &output, "not a func") + + assert.EqualError(t, err, "groupFn should to be a function") + }) + + t.Run(fmt.Sprintf("GroupBy should return err if predicate function do not take exactly one argument"), func(t *testing.T) { + in := []Person{} + var out map[int][]Person + + { + err := godash.GroupBy(in, &out, func() {}) + + assert.EqualError(t, err, "group function has to take only one argument") + } + + { + err := godash.GroupBy(in, &out, func(int, int) {}) + + assert.EqualError(t, err, "group function has to take only one argument") + } + + }) + + t.Run(fmt.Sprintf("GroupBy should return err if group function do not return exactly one value"), func(t *testing.T) { + in := []Person{} + var out map[int][]Person + + { + err := godash.GroupBy(in, &out, func(int) {}) + + assert.EqualError(t, err, "group function should return only one return value") + } + { + err := godash.GroupBy(in, &out, func(int) (bool, bool) { return true, true }) + + assert.EqualError(t, err, "group function should return only one return value") + + } + }) + + t.Run(fmt.Sprintf("GroupBy should return err if output is not the pointer to the map"), func(t *testing.T) { + in := []Person{} + var out []Person + + { + err := godash.GroupBy(in, &out, func(int) bool { return true }) + + assert.EqualError(t, err, "output has to be a map") + } + }) + + t.Run(fmt.Sprintf("GroupBy should return err if the type's return of group function is not the same type of the key of output"), func(t *testing.T) { + in := []Person{} + var out map[int][]Person + + { + err := godash.GroupBy(in, &out, func(int) bool { return true }) + + assert.EqualError(t, err, "group function should return the type of key's output") + } + }) + + t.Run(fmt.Sprintf("GroupBy should return err if the type of value's output is not a slice"), func(t *testing.T) { + in := []Person{} + var out map[int]Person + + { + err := godash.GroupBy(in, &out, func(person Person) int { + return person.age + }) + + assert.EqualError(t, err, "The type of value's output should be a slice") + } + }) + + t.Run(fmt.Sprintf("GroupBy should return err if the type of element of value's slice is not a same the type of input"), func(t *testing.T) { + in := []Person{} + var out map[int][]int + + { + err := godash.GroupBy(in, &out, func(person Person) int { + return person.age + }) + + assert.EqualError(t, err, "The type of element of value's slice has to be a same the type of input") + } + }) + + t.Run("GroupBy should return err if the type of element of input is not a same the type of groupFn input", func(t *testing.T) { + in := []Person{} + var out map[Person][]Person + + { + err := godash.GroupBy(in, &out, func(i int) Person { + return Person{} + }) + + assert.EqualError(t, err, "group function's argument (int) has to be (godash_test.Person)") + } + }) } func ExampleGroupBy() { - type Person struct { - name string - age int - } + john := Person{name: "John", age: 25} doe := Person{name: "Doe", age: 30} wick := Person{name: "Wick", age: 25} @@ -42,4 +150,5 @@ func ExampleGroupBy() { // Groups Count: 2 // Group 25: John, Wick, // Group 30: Doe, + }