diff --git a/README.md b/README.md index a0bdce5..88d3d5f 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Inspired from [Lodash](https://github.com/lodash/lodash) for golang 4. [Any](#Any-or-Some) or [Some](#Any-or-Some) 5. [Find](#Find) 6. [All](#All-or-Every) or [Every](#All-or-Every) +7. [GroupBy](#GroupBy) ## Usages @@ -223,3 +224,38 @@ func main() { }) fmt.Println(output) // prints false } +``` + +### 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 new file mode 100644 index 0000000..1aa9dd5 --- /dev/null +++ b/groupBy.go @@ -0,0 +1,98 @@ +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 slice. 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 should to be a function") + } + + 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} + key := group.Call(argValues)[0] + + if slice := result.MapIndex(key); slice.IsValid() { + slice = reflect.Append(slice, argValues[0]) + result.SetMapIndex(key, slice) + + } else { + slice := reflect.MakeSlice(outputSliceType, 0, input.Len()) + slice = reflect.Append(slice, argValues[0]) + result.SetMapIndex(key, 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..c65a1b8 --- /dev/null +++ b/groupBy_test.go @@ -0,0 +1,154 @@ +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() { + + 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, + +}