Skip to content

Commit d02a012

Browse files
authored
Merge pull request thoas#113 from vellotis/feature-predicates-for-contains-indexof-lastindexof
Add possibility to use predicate for `Contains`, `IndexOf` & `LastIndexOf` functions
2 parents bd86b00 + 5b72292 commit d02a012

File tree

9 files changed

+185
-10
lines changed

9 files changed

+185
-10
lines changed

README.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ this can be replaced by ``funk.Contains``:
109109
110110
// slice of Foo ptr
111111
funk.Contains([]*Foo{f}, f) // true
112+
funk.Contains([]*Foo{f}, func (foo *Foo) bool {
113+
return foo.ID == f.ID
114+
}) // true
112115
funk.Contains([]*Foo{f}, nil) // false
113116
114117
b := &Foo{
@@ -126,6 +129,9 @@ this can be replaced by ``funk.Contains``:
126129
127130
// even map
128131
funk.Contains(map[int]string{1: "Florent"}, 1) // true
132+
funk.Contains(map[int]string{1: "Florent"}, func(key int, name string) bool {
133+
return key == 1 // or `name == "Florent"` for the value type
134+
}) // true
129135
130136
see also, typesafe implementations: ContainsInt_, ContainsInt64_, ContainsFloat32_, ContainsFloat64_, ContainsString_
131137

@@ -175,6 +181,9 @@ if the value cannot be found.
175181
176182
// slice of string
177183
funk.IndexOf([]string{"foo", "bar"}, "bar") // 1
184+
funk.IndexOf([]string{"foo", "bar"}, func(value string) bool {
185+
return value == "bar"
186+
}) // 1
178187
funk.IndexOf([]string{"foo", "bar"}, "gilles") // -1
179188
180189
see also, typesafe implementations: IndexOfInt_, IndexOfInt64_, IndexOfFloat32_, IndexOfFloat64_, IndexOfString_
@@ -195,6 +204,9 @@ if the value cannot be found.
195204
196205
// slice of string
197206
funk.LastIndexOf([]string{"foo", "bar", "bar"}, "bar") // 2
207+
funk.LastIndexOf([]string{"foo", "bar"}, func(value string) bool {
208+
return value == "bar"
209+
}) // 2
198210
funk.LastIndexOf([]string{"foo", "bar"}, "gilles") // -1
199211
200212
see also, typesafe implementations: LastIndexOfInt_, LastIndexOfInt64_, LastIndexOfFloat32_, LastIndexOfFloat64_, LastIndexOfString_

chain_builder_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,12 @@ func TestChainContains(t *testing.T) {
491491
In: []string{"foo", "bar"},
492492
Contains: "bar",
493493
},
494+
{
495+
In: []string{"foo", "bar"},
496+
Contains: func (value string) bool {
497+
return value == "bar"
498+
},
499+
},
494500
{
495501
In: results,
496502
Contains: f,
@@ -519,6 +525,12 @@ func TestChainContains(t *testing.T) {
519525
In: map[int]*Foo{1: f, 3: c},
520526
Contains: 2,
521527
},
528+
{
529+
In: map[int]*Foo{1: f, 3: c},
530+
Contains: func (key int, foo *Foo) bool {
531+
return key == 3 && foo.FirstName == "Harald"
532+
},
533+
},
522534
}
523535

524536
for idx, tc := range testCases {
@@ -810,6 +822,12 @@ func TestChainIndexOf(t *testing.T) {
810822
In: []string{"foo", "bar"},
811823
Item: "bar",
812824
},
825+
{
826+
In: []string{"foo", "bar"},
827+
Item: func (value string) bool {
828+
return value == "bar"
829+
},
830+
},
813831
{
814832
In: results,
815833
Item: f,
@@ -888,6 +906,12 @@ func TestChainLastIndexOf(t *testing.T) {
888906
In: []string{"foo", "bar", "bar"},
889907
Item: "bar",
890908
},
909+
{
910+
In: []string{"foo", "bar", "bar"},
911+
Item: func (value string) bool {
912+
return value == "bar"
913+
},
914+
},
891915
{
892916
In: []int{1, 2, 2, 3},
893917
Item: 2,

helpers.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func PtrOf(itf interface{}) interface{} {
7575
func IsFunction(in interface{}, num ...int) bool {
7676
funcType := reflect.TypeOf(in)
7777

78-
result := funcType.Kind() == reflect.Func
78+
result := funcType != nil && funcType.Kind() == reflect.Func
7979

8080
if len(num) >= 1 {
8181
result = result && funcType.NumIn() == num[0]
@@ -88,6 +88,27 @@ func IsFunction(in interface{}, num ...int) bool {
8888
return result
8989
}
9090

91+
// IsPredicate returns if the argument is a predicate function.
92+
func IsPredicate(in interface{}, inTypes ...reflect.Type) bool {
93+
if len(inTypes) == 0 {
94+
inTypes = append(inTypes, nil)
95+
}
96+
97+
funcType := reflect.TypeOf(in)
98+
99+
result := funcType != nil && funcType.Kind() == reflect.Func
100+
101+
result = result && funcType.NumOut() == 1 && funcType.Out(0).Kind() == reflect.Bool
102+
result = result && funcType.NumIn() == len(inTypes)
103+
104+
for i := 0; result && i < len(inTypes); i++ {
105+
inType := inTypes[i]
106+
result = inType == nil || inType.ConvertibleTo(funcType.In(i))
107+
}
108+
109+
return result
110+
}
111+
91112
// IsEqual returns if the two objects are equal
92113
func IsEqual(expected interface{}, actual interface{}) bool {
93114
if expected == nil || actual == nil {

helpers_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,32 @@ func TestIsIteratee(t *testing.T) {
269269

270270
is.False(IsIteratee(nil))
271271
}
272+
273+
func TestIsFunction(t *testing.T) {
274+
is := assert.New(t)
275+
276+
is.False(IsFunction(nil))
277+
is.False(IsFunction(""))
278+
is.True(IsFunction(func() {}))
279+
is.True(IsFunction(func(string, string, string) bool { return false }, 3))
280+
is.False(IsFunction(func(string, string, string) bool { return false }, 3, 0))
281+
is.True(IsFunction(func(string, string, string) (bool, error) { return false, nil }, 3, 2))
282+
}
283+
284+
func TestIsPredicate(t *testing.T) {
285+
is := assert.New(t)
286+
287+
is.False(IsPredicate(nil))
288+
is.False(IsPredicate(""))
289+
is.False(IsPredicate(func() {}))
290+
is.False(IsPredicate(func() bool { return false}))
291+
is.True(IsPredicate(func(int) bool { return false}))
292+
is.True(IsPredicate(func(int) bool { return false}, nil))
293+
is.False(IsPredicate(func(int) bool { return false}, reflect.TypeOf("")))
294+
is.True(IsPredicate(func(int) bool { return false}, reflect.TypeOf(0)))
295+
is.False(IsPredicate(func(int, string) bool { return false}, reflect.TypeOf("")))
296+
is.False(IsPredicate(func(int, string) bool { return false}, reflect.TypeOf(""), reflect.TypeOf(0)))
297+
is.True(IsPredicate(func(int, string) bool { return false}, reflect.TypeOf(0), reflect.TypeOf("")))
298+
is.False(IsPredicate(func(struct{}, string) bool { return false}, reflect.TypeOf(0), reflect.TypeOf("")))
299+
is.True(IsPredicate(func(struct{}, string) bool { return false}, nil, reflect.TypeOf("")))
300+
}

lazy_builder_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,12 @@ func TestLazyContains(t *testing.T) {
491491
In: []string{"foo", "bar"},
492492
Contains: "bar",
493493
},
494+
{
495+
In: []string{"foo", "bar"},
496+
Contains: func (value string) bool {
497+
return value == "bar"
498+
},
499+
},
494500
{
495501
In: results,
496502
Contains: f,
@@ -519,6 +525,12 @@ func TestLazyContains(t *testing.T) {
519525
In: map[int]*Foo{1: f, 3: c},
520526
Contains: 2,
521527
},
528+
{
529+
In: map[int]*Foo{1: f, 3: c},
530+
Contains: func (key int, foo *Foo) bool {
531+
return key == 3 && foo.FirstName == "Harald"
532+
},
533+
},
522534
}
523535

524536
for idx, tc := range testCases {
@@ -810,6 +822,12 @@ func TestLazyIndexOf(t *testing.T) {
810822
In: []string{"foo", "bar"},
811823
Item: "bar",
812824
},
825+
{
826+
In: []string{"foo", "bar"},
827+
Item: func (value string) bool {
828+
return value == "bar"
829+
},
830+
},
813831
{
814832
In: results,
815833
Item: f,
@@ -888,6 +906,12 @@ func TestLazyLastIndexOf(t *testing.T) {
888906
In: []string{"foo", "bar", "bar"},
889907
Item: "bar",
890908
},
909+
{
910+
In: []string{"foo", "bar", "bar"},
911+
Item: func (value string) bool {
912+
return value == "bar"
913+
},
914+
},
891915
{
892916
In: []int{1, 2, 2, 3},
893917
Item: 2,

predicate.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func predicatesImpl(value interface{}, wantedAnswer bool, predicates interface{}
2020
}
2121

2222
funcType := funcValue.Type()
23-
if funcType.NumIn() != 1 || funcType.NumOut() != 1 || funcType.Out(0).Kind() != reflect.Bool {
23+
if !IsPredicate(funcValue.Interface()) {
2424
panic("Predicate function must have 1 parameter and must return boolean")
2525
}
2626

presence.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,9 @@ func IndexOf(in interface{}, elem interface{}) int {
118118
}
119119

120120
if inType.Kind() == reflect.Slice {
121+
equalTo := equal(elem)
121122
for i := 0; i < inValue.Len(); i++ {
122-
if equal(inValue.Index(i).Interface(), elem) {
123+
if equalTo(reflect.Value{}, inValue.Index(i)) {
123124
return i
124125
}
125126
}
@@ -144,8 +145,9 @@ func LastIndexOf(in interface{}, elem interface{}) int {
144145
if inType.Kind() == reflect.Slice {
145146
length := inValue.Len()
146147

148+
equalTo := equal(elem)
147149
for i := length - 1; i >= 0; i-- {
148-
if equal(inValue.Index(i).Interface(), elem) {
150+
if equalTo(reflect.Value{}, inValue.Index(i)) {
149151
return i
150152
}
151153
}
@@ -164,14 +166,16 @@ func Contains(in interface{}, elem interface{}) bool {
164166
case reflect.String:
165167
return strings.Contains(inValue.String(), elemValue.String())
166168
case reflect.Map:
169+
equalTo := equal(elem, true)
167170
for _, key := range inValue.MapKeys() {
168-
if equal(key.Interface(), elem) {
171+
if equalTo(key, inValue.MapIndex(key)) {
169172
return true
170173
}
171174
}
172175
case reflect.Slice, reflect.Array:
176+
equalTo := equal(elem)
173177
for i := 0; i < inValue.Len(); i++ {
174-
if equal(inValue.Index(i).Interface(), elem) {
178+
if equalTo(reflect.Value{}, inValue.Index(i)) {
175179
return true
176180
}
177181
}

presence_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,20 @@ func TestContains(t *testing.T) {
5454

5555
is.True(Contains(mapping, 1))
5656
is.False(Contains(mapping, 2))
57+
58+
is.False(Contains(mapping, func (key int, val *Foo) bool {
59+
return key == 4
60+
}))
61+
is.True(Contains(mapping, func (key int, val *Foo) bool {
62+
return key == 1
63+
}))
64+
65+
is.False(Contains(mapping, func (_ int, val *Foo) bool {
66+
return val.FirstName == "NotPresent"
67+
}))
68+
is.True(Contains(mapping, func (_ int, val *Foo) bool {
69+
return val.FirstName == "Harald"
70+
}))
5771
}
5872

5973
func TestEvery(t *testing.T) {
@@ -117,6 +131,9 @@ func TestIndexOf(t *testing.T) {
117131
is := assert.New(t)
118132

119133
is.Equal(IndexOf([]string{"foo", "bar"}, "bar"), 1)
134+
is.Equal(IndexOf([]string{"foo", "bar"}, func (value string) bool {
135+
return value == "bar"
136+
}), 1)
120137

121138
is.Equal(IndexOf(results, f), 0)
122139
is.Equal(IndexOf(results, b), -1)
@@ -126,6 +143,9 @@ func TestLastIndexOf(t *testing.T) {
126143
is := assert.New(t)
127144

128145
is.Equal(LastIndexOf([]string{"foo", "bar", "bar"}, "bar"), 2)
146+
is.Equal(LastIndexOf([]string{"foo", "bar", "bar"}, func (value string) bool {
147+
return value == "bar"
148+
}), 2)
129149
is.Equal(LastIndexOf([]int{1, 2, 2, 3}, 2), 2)
130150
is.Equal(LastIndexOf([]int{1, 2, 2, 3}, 4), -1)
131151
}

utils.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,57 @@
11
package funk
22

33
import (
4+
"fmt"
45
"reflect"
56
)
67

7-
func equal(expected, actual interface{}) bool {
8-
if expected == nil || actual == nil {
9-
return expected == actual
8+
func equal(expectedOrPredicate interface{}, optionalIsMap ...bool) func(keyValueIfMap, actualValue reflect.Value) bool {
9+
isMap := append(optionalIsMap, false)[0]
10+
11+
if IsFunction(expectedOrPredicate) {
12+
inTypes := []reflect.Type{nil}; if isMap {
13+
inTypes = append(inTypes, nil)
14+
}
15+
16+
if !IsPredicate(expectedOrPredicate, inTypes...) {
17+
panic(fmt.Sprintf("Predicate function must have %d parameter and must return boolean", len(inTypes)))
18+
}
19+
20+
predicateValue := reflect.ValueOf(expectedOrPredicate)
21+
22+
return func(keyValueIfMap, actualValue reflect.Value) bool {
23+
24+
if isMap && !keyValueIfMap.Type().ConvertibleTo(predicateValue.Type().In(0)) {
25+
panic("Given key is not compatible with type of parameter for the predicate.")
26+
}
27+
28+
if (isMap && !actualValue.Type().ConvertibleTo(predicateValue.Type().In(1))) ||
29+
(!isMap && !actualValue.Type().ConvertibleTo(predicateValue.Type().In(0))) {
30+
panic("Given value is not compatible with type of parameter for the predicate.")
31+
}
32+
33+
args := []reflect.Value{actualValue}
34+
if isMap {
35+
args = append([]reflect.Value{keyValueIfMap}, args...)
36+
}
37+
38+
return predicateValue.Call(args)[0].Bool()
39+
}
1040
}
1141

12-
return reflect.DeepEqual(expected, actual)
42+
expected := expectedOrPredicate
43+
44+
return func(keyValueIfMap, actualValue reflect.Value) bool {
45+
if isMap {
46+
actualValue = keyValueIfMap
47+
}
48+
49+
if expected == nil || actualValue.IsZero() {
50+
return actualValue.Interface() == expected
51+
}
1352

53+
return reflect.DeepEqual(actualValue.Interface(), expected)
54+
}
1455
}
1556

1657
func sliceElem(rtype reflect.Type) reflect.Type {

0 commit comments

Comments
 (0)