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 JoinOn and GroupJoinOn query methods #111

Open
wants to merge 1 commit 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
91 changes: 91 additions & 0 deletions groupjoinon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package linq

import "reflect"

// GroupJoinOn correlates the elements of two collections based on a predicate,
// and groups the results.
//
// GroupJoinOn is a more general form of GroupJoin in which the predicate
// function can be thought of as
// outerKeySelector(outerItem) == innerKeySelector(innerItem)
//
// This method produces hierarchical results, which means that elements from
// outer query are paired with collections of matching elements from inner.
// GroupJoinOn enables you to base your results on a whole set of matches for each
// element of outer query.
//
// The resultSelector function is called only one time for each outer element
// together with a collection of all the inner elements that match the outer
// element. This differs from the JoinOn method, in which the result selector
// function is invoked on pairs that contain one element from outer and one
// element from inner.
//
// GroupJoinOn preserves the order of the elements of outer, and for each element
// of outer, the order of the matching elements from inner.
func (q Query) GroupJoinOn(inner Query,
onPredicate func(outer interface{}, inner interface{}) bool,
resultSelector func(outer interface{}, inners []interface{}) interface{}) Query {

return Query{
Iterate: func() Iterator {
outernext := q.Iterate()

return func() (item interface{}, ok bool) {
outerItem, outerOk := outernext()
if !outerOk {
return
}
innernext := inner.Iterate()
var group []interface{}
for innerItem, ok := innernext(); ok; innerItem, ok = innernext() {
if onPredicate(outerItem, innerItem) {
group = append(group, innerItem)
}
}
item = resultSelector(outerItem, group)
return item, true
}
},
}
}

// GroupJoinOnT is the typed version of GroupJoinOn.
//
// - inner: The query to join to the outer query.
// - onPredicateFn is of type "func(TOuter, TInner) bool"
// - resultSelectorFn: is of type "func(TOuter, inners []TInner) TResult"
//
// NOTE: GroupJoinOn has better performance than GroupJoinOnT.
func (q Query) GroupJoinOnT(inner Query,
onPredicateFn interface{},
resultSelectorFn interface{}) Query {
onPredicateGenericFunc, err := newGenericFunc(
"GroupJoinOnT", "onPredicateFn", onPredicateFn,
simpleParamValidator(newElemTypeSlice(new(genericType), new(genericType)), newElemTypeSlice(new(bool))),
)
if err != nil {
panic(err)
}

onPredicateFunc := func(outerItem interface{}, innerItem interface{}) bool {
return onPredicateGenericFunc.Call(outerItem, innerItem).(bool)
}

resultSelectorGenericFunc, err := newGenericFunc(
"GroupJoinOnT", "resultSelectorFn", resultSelectorFn,
simpleParamValidator(newElemTypeSlice(new(genericType), new(genericType)), newElemTypeSlice(new(genericType))),
)
if err != nil {
panic(err)
}

resultSelectorFunc := func(outer interface{}, inners []interface{}) interface{} {
innerSliceType := reflect.MakeSlice(resultSelectorGenericFunc.Cache.TypesIn[1], 0, 0)
innersSlicePointer := reflect.New(innerSliceType.Type())
From(inners).ToSlice(innersSlicePointer.Interface())
innersTyped := reflect.Indirect(innersSlicePointer).Interface()
return resultSelectorGenericFunc.Call(outer, innersTyped)
}

return q.GroupJoinOn(inner, onPredicateFunc, resultSelectorFunc)
}
67 changes: 67 additions & 0 deletions groupjoinon_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package linq

import (
"testing"
)

func TestGroupJoinOn(t *testing.T) {
outer := []int{0, 1, 2}
inner := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
want := []interface{}{
KeyValue{0, 9},
KeyValue{1, 8},
KeyValue{2, 7},
}

q := From(outer).GroupJoinOn(
From(inner),
func(i interface{}, j interface{}) bool { return i.(int) < j.(int) },
func(outer interface{}, inners []interface{}) interface{} {
return KeyValue{outer, len(inners)}
})

if !validateQuery(q, want) {
t.Errorf("From().GroupJoinOn()=%v expected %v", toSlice(q), want)
}
}

func TestGroupJoinOnT(t *testing.T) {
outer := []int{0, 1, 2}
inner := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
want := []interface{}{
KeyValue{0, 9},
KeyValue{1, 8},
KeyValue{2, 7},
}

q := From(outer).GroupJoinOnT(
From(inner),
func(i, j int) bool { return i < j },
func(outer int, inners []int) KeyValue {
return KeyValue{outer, len(inners)}
})

if !validateQuery(q, want) {
t.Errorf("From().GroupJoinOnT()=%v expected %v", toSlice(q), want)
}
}

func TestGroupJoinOnT_PanicWhenOnPredicateFnIsInvalid(t *testing.T) {
mustPanicWithError(t, "GroupJoinOnT: parameter [onPredicateFn] has a invalid function signature. Expected: 'func(T,T)bool', actual: 'func(int,int)int'", func() {
From([]int{0, 1, 2}).GroupJoinOnT(
From([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}),
func(i, j int) int { return i + j },
func(outer int, inners []int) KeyValue { return KeyValue{outer, len(inners)} },
)
})
}

func TestGroupJoinOnT_PanicWhenResultSelectorFnIsInvalid(t *testing.T) {
mustPanicWithError(t, "GroupJoinOnT: parameter [resultSelectorFn] has a invalid function signature. Expected: 'func(T,T)T', actual: 'func(int,int,[]int)linq.KeyValue'", func() {
From([]int{0, 1, 2}).GroupJoinOnT(
From([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}),
func(i, j int) bool { return i+j == 5 },
func(outer, j int, inners []int) KeyValue { return KeyValue{outer, len(inners)} },
)
})
}
1 change: 1 addition & 0 deletions join.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func (q Query) Join(inner Query,

// JoinT is the typed version of Join.
//
// - inner: The query to join to the outer query.
// - outerKeySelectorFn is of type "func(TOuter) TKey"
// - innerKeySelectorFn is of type "func(TInner) TKey"
// - resultSelectorFn is of type "func(TOuter,TInner) TResult"
Expand Down
77 changes: 77 additions & 0 deletions joinon.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package linq

// JoinOn correlates the elements of two collections based on a predicate function.
//
// A joinOn refers to the operation of correlating the elements of two sources of
// information based on a predicate function that returns a bool given a pair of
// outer and inner collection elements. JoinOn is a more general form of Join
// in which the predicate function can be thought of as
// outerKeySelector(outerItem) == innerKeySelector(innerItem)
//
// JoinOn preserves the order of the elements of outer collection, and for each of
// these elements, the order of the matching elements of inner.
func (q Query) JoinOn(inner Query,
onPredicate func(outer interface{}, inner interface{}) bool,
resultSelector func(outer interface{}, inner interface{}) interface{}) Query {

return Query{
Iterate: func() Iterator {
outernext := q.Iterate()
innernext := inner.Iterate()

outerItem, outerOk := outernext()

return func() (item interface{}, ok bool) {

for outerOk {
for innerItem, ok := innernext(); ok; innerItem, ok = innernext() {
if onPredicate(outerItem, innerItem) {
item = resultSelector(outerItem, innerItem)
return item, true
}
}
innernext = inner.Iterate()
outerItem, outerOk = outernext()
}
return
}
},
}
}

// JoinOnT is the typed version of JoinOn.
//
// - inner: The query to join to the outer query.
// - onPredicateFn is of type "func(TOuter, TInner) bool"
// - resultSelectorFn is of type "func(TOuter,TInner) TResult"
//
// NOTE: JoinOn has better performance than JoinOnT.
func (q Query) JoinOnT(inner Query,
onPredicateFn interface{},
resultSelectorFn interface{}) Query {
onPredicateGenericFunc, err := newGenericFunc(
"JoinOnT", "onPredicateFn", onPredicateFn,
simpleParamValidator(newElemTypeSlice(new(genericType), new(genericType)), newElemTypeSlice(new(bool))),
)
if err != nil {
panic(err)
}

onPredicateFunc := func(outerItem interface{}, innerItem interface{}) bool {
return onPredicateGenericFunc.Call(outerItem, innerItem).(bool)
}

resultSelectorGenericFunc, err := newGenericFunc(
"JoinOnT", "resultSelectorFn", resultSelectorFn,
simpleParamValidator(newElemTypeSlice(new(genericType), new(genericType)), newElemTypeSlice(new(genericType))),
)
if err != nil {
panic(err)
}

resultSelectorFunc := func(outer interface{}, inner interface{}) interface{} {
return resultSelectorGenericFunc.Call(outer, inner)
}

return q.JoinOn(inner, onPredicateFunc, resultSelectorFunc)
}
65 changes: 65 additions & 0 deletions joinon_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package linq

import (
"testing"
)

func TestJoinOn(t *testing.T) {
outer := []int{0, 1}
inner := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
want := []interface{}{
KeyValue{0, 5},
KeyValue{1, 4},
}

q := From(outer).JoinOn(
From(inner),
func(i interface{}, j interface{}) bool { return i.(int)+j.(int) == 5 },
func(outer interface{}, inner interface{}) interface{} {
return KeyValue{outer, inner}
})

if !validateQuery(q, want) {
t.Errorf("From().JoinOn()=%v expected %v", toSlice(q), want)
}
}

func TestJoinOnT(t *testing.T) {
outer := []int{0, 1}
inner := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
want := []interface{}{
KeyValue{0, 5},
KeyValue{1, 4},
}

q := From(outer).JoinOnT(
From(inner),
func(i, j int) bool { return i+j == 5 },
func(outer int, inner int) KeyValue {
return KeyValue{outer, inner}
})

if !validateQuery(q, want) {
t.Errorf("From().JoinOnT()=%v expected %v", toSlice(q), want)
}
}

func TestJoinOnT_PanicWhenOnPredicateFnIsInvalid(t *testing.T) {
mustPanicWithError(t, "JoinOnT: parameter [onPredicateFn] has a invalid function signature. Expected: 'func(T,T)bool', actual: 'func(int,int)int'", func() {
From([]int{0, 1}).JoinOnT(
From([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}),
func(i, j int) int { return i + j },
func(outer int, inner int) KeyValue { return KeyValue{outer, inner} },
)
})
}

func TestJoinOnT_PanicWhenResultSelectorFnIsInvalid(t *testing.T) {
mustPanicWithError(t, "JoinOnT: parameter [resultSelectorFn] has a invalid function signature. Expected: 'func(T,T)T', actual: 'func(int,int,int)linq.KeyValue'", func() {
From([]int{0, 1, 2}).JoinOnT(
From([]int{1, 2, 3, 4, 5, 6, 7, 8, 9}),
func(i, j int) bool { return i == j%2 },
func(outer int, inner, j int) KeyValue { return KeyValue{outer, inner} },
)
})
}