From c0c110e9c83fdf501283c63f23f1dab74c0f7b74 Mon Sep 17 00:00:00 2001 From: Onur Derin Date: Tue, 24 May 2022 00:21:08 +0200 Subject: [PATCH] Add JoinOn and GroupJoinOn query methods --- groupjoinon.go | 91 +++++++++++++++++++++++++++++++++++++++++++++ groupjoinon_test.go | 67 +++++++++++++++++++++++++++++++++ join.go | 1 + joinon.go | 77 ++++++++++++++++++++++++++++++++++++++ joinon_test.go | 65 ++++++++++++++++++++++++++++++++ 5 files changed, 301 insertions(+) create mode 100644 groupjoinon.go create mode 100644 groupjoinon_test.go create mode 100644 joinon.go create mode 100644 joinon_test.go diff --git a/groupjoinon.go b/groupjoinon.go new file mode 100644 index 0000000..8768006 --- /dev/null +++ b/groupjoinon.go @@ -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) +} diff --git a/groupjoinon_test.go b/groupjoinon_test.go new file mode 100644 index 0000000..b9e9ef6 --- /dev/null +++ b/groupjoinon_test.go @@ -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)} }, + ) + }) +} diff --git a/join.go b/join.go index 68211a2..1a463c4 100644 --- a/join.go +++ b/join.go @@ -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" diff --git a/joinon.go b/joinon.go new file mode 100644 index 0000000..046adf8 --- /dev/null +++ b/joinon.go @@ -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) +} diff --git a/joinon_test.go b/joinon_test.go new file mode 100644 index 0000000..e62b473 --- /dev/null +++ b/joinon_test.go @@ -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} }, + ) + }) +}