Skip to content
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

Add GroupBy function and example run #16

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
}
```
98 changes: 98 additions & 0 deletions groupBy.go
Original file line number Diff line number Diff line change
@@ -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)
}
154 changes: 154 additions & 0 deletions groupBy_test.go
Original file line number Diff line number Diff line change
@@ -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,

}