added float support and cleaner code
This commit is contained in:
parent
da12fce77a
commit
0ebcd6967b
5 changed files with 208 additions and 159 deletions
60
README.md
60
README.md
|
|
@ -35,12 +35,25 @@ var data = Filter{
|
|||
}
|
||||
```
|
||||
|
||||
### Struct fields
|
||||
|
||||
Supported struct field values are:
|
||||
|
||||
* int, int8, int16, int32, int64
|
||||
* float32, float64
|
||||
* string
|
||||
* bool
|
||||
|
||||
For nested structs:
|
||||
|
||||
* struct
|
||||
|
||||
### Filter logic
|
||||
|
||||
Supported logical matching:
|
||||
|
||||
* AND - all of the queries have to match
|
||||
* OR - only one query has to match
|
||||
* OR - only one query has to match
|
||||
* NOT - none of the queries have to match
|
||||
|
||||
### Explicit match
|
||||
|
|
@ -79,22 +92,31 @@ data = [{2str 42 true {s2 {2}}}]
|
|||
|
||||
```
|
||||
go test -cpu 1,2,4 --bench .
|
||||
BenchmarkFilterSimple 2000000 877 ns/op 432 B/op 12 allocs/op
|
||||
BenchmarkFilterSimple-2 2000000 865 ns/op 432 B/op 12 allocs/op
|
||||
BenchmarkFilterSimple-4 1000000 1713 ns/op 432 B/op 12 allocs/op
|
||||
BenchmarkFilterComplex 500000 3032 ns/op 672 B/op 48 allocs/op
|
||||
BenchmarkFilterComplex-2 500000 3100 ns/op 672 B/op 48 allocs/op
|
||||
BenchmarkFilterComplex-4 500000 6235 ns/op 672 B/op 48 allocs/op
|
||||
BenchmarkFilterComplexOr 500000 2649 ns/op 816 B/op 39 allocs/op
|
||||
BenchmarkFilterComplexOr-2 500000 2659 ns/op 816 B/op 39 allocs/op
|
||||
BenchmarkFilterComplexOr-4 500000 5548 ns/op 816 B/op 39 allocs/op
|
||||
BenchmarkGenericFilterSimple 500000 2340 ns/op 1200 B/op 24 allocs/op
|
||||
BenchmarkGenericFilterSimple-2 1000000 2307 ns/op 1200 B/op 24 allocs/op
|
||||
BenchmarkGenericFilterSimple-4 1000000 3615 ns/op 1200 B/op 24 allocs/op
|
||||
BenchmarkGenericFilterComplex 200000 7073 ns/op 1680 B/op 96 allocs/op
|
||||
BenchmarkGenericFilterComplex-2 200000 7598 ns/op 1680 B/op 96 allocs/op
|
||||
BenchmarkGenericFilterComplex-4 200000 10735 ns/op 1680 B/op 96 allocs/op
|
||||
BenchmarkGenericFilterComplexOr 200000 5406 ns/op 2456 B/op 61 allocs/op
|
||||
BenchmarkGenericFilterComplexOr-2 300000 5300 ns/op 2456 B/op 61 allocs/op
|
||||
BenchmarkGenericFilterComplexOr-4 200000 7707 ns/op 2456 B/op 61 allocs/op
|
||||
BenchmarkNewQuerySimple 3000000 401 ns/op 176 B/op 7 allocs/op
|
||||
BenchmarkNewQuerySimple-2 5000000 390 ns/op 176 B/op 7 allocs/op
|
||||
BenchmarkNewQuerySimple-4 2000000 792 ns/op 176 B/op 7 allocs/op
|
||||
BenchmarkNewQueryComplex 1000000 1416 ns/op 656 B/op 17 allocs/op
|
||||
BenchmarkNewQueryComplex-2 1000000 1384 ns/op 656 B/op 17 allocs/op
|
||||
BenchmarkNewQueryComplex-4 1000000 2488 ns/op 656 B/op 17 allocs/op
|
||||
BenchmarkNewQueryComplexOr 1000000 1192 ns/op 640 B/op 17 allocs/op
|
||||
BenchmarkNewQueryComplexOr-2 1000000 1158 ns/op 640 B/op 17 allocs/op
|
||||
BenchmarkNewQueryComplexOr-4 1000000 1963 ns/op 640 B/op 17 allocs/op
|
||||
BenchmarkFilterSimple 2000000 907 ns/op 432 B/op 12 allocs/op
|
||||
BenchmarkFilterSimple-2 2000000 899 ns/op 432 B/op 12 allocs/op
|
||||
BenchmarkFilterSimple-4 1000000 1468 ns/op 432 B/op 12 allocs/op
|
||||
BenchmarkFilterComplex 500000 3129 ns/op 672 B/op 48 allocs/op
|
||||
BenchmarkFilterComplex-2 500000 3208 ns/op 672 B/op 48 allocs/op
|
||||
BenchmarkFilterComplex-4 500000 6644 ns/op 672 B/op 48 allocs/op
|
||||
BenchmarkFilterComplexOr 500000 2698 ns/op 816 B/op 39 allocs/op
|
||||
BenchmarkFilterComplexOr-2 500000 2832 ns/op 816 B/op 39 allocs/op
|
||||
BenchmarkFilterComplexOr-4 500000 5509 ns/op 816 B/op 39 allocs/op
|
||||
BenchmarkGenericFilterSimple 500000 2386 ns/op 1200 B/op 24 allocs/op
|
||||
BenchmarkGenericFilterSimple-2 1000000 2360 ns/op 1200 B/op 24 allocs/op
|
||||
BenchmarkGenericFilterSimple-4 1000000 3656 ns/op 1200 B/op 24 allocs/op
|
||||
BenchmarkGenericFilterComplex 200000 7178 ns/op 1680 B/op 96 allocs/op
|
||||
BenchmarkGenericFilterComplex-2 200000 7102 ns/op 1680 B/op 96 allocs/op
|
||||
BenchmarkGenericFilterComplex-4 200000 12602 ns/op 1680 B/op 96 allocs/op
|
||||
BenchmarkGenericFilterComplexOr 200000 5454 ns/op 2456 B/op 61 allocs/op
|
||||
BenchmarkGenericFilterComplexOr-2 300000 5444 ns/op 2456 B/op 61 allocs/op
|
||||
BenchmarkGenericFilterComplexOr-4 300000 9970 ns/op 2456 B/op 61 allocs/op
|
||||
```
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ var data = Filter{
|
|||
{"str1", 42, true, Struct1{"s1", Struct2{1}}},
|
||||
{"2str", 42, true, Struct1{"s2", Struct2{2}}},
|
||||
{"str3", 42, false, Struct1{"s3", Struct2{3}}},
|
||||
{":", 42, false, Struct1{"s3", Struct2{3}}},
|
||||
{":*", 42, false, Struct1{"s3", Struct2{3}}},
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
|
|
|||
167
filter.go
167
filter.go
|
|
@ -6,12 +6,9 @@ package filter
|
|||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const any = '*'
|
||||
|
||||
// Filter is the filter interface to be implemented for faster non generic filtering
|
||||
// Example implementation:
|
||||
// type f []struct
|
||||
|
|
@ -26,7 +23,8 @@ type Filter interface {
|
|||
|
||||
type match struct {
|
||||
fields []string
|
||||
value string
|
||||
mf func(string) bool
|
||||
bf func(bool) bool
|
||||
}
|
||||
|
||||
// Logic is the filter logic
|
||||
|
|
@ -50,7 +48,7 @@ type Query struct {
|
|||
// NewQuery parses the filter query and match logic
|
||||
// Examples:
|
||||
// Explicit match: <struct field>:<struct value>
|
||||
// Wildcard match:
|
||||
// Wildcard match (use ** to match the '*' character):
|
||||
// <struct field>:*<part of struct value>
|
||||
// <struct field>:<part of struct value>*
|
||||
// <struct field>:*<middl of struct value>*
|
||||
|
|
@ -58,13 +56,17 @@ type Query struct {
|
|||
// <struct field>.<struct field>:<struct value>
|
||||
// Multiple matches:
|
||||
// <struct field>:<struct value> <struct field>:<struct value>*
|
||||
// Supported data types are: string, int, bool
|
||||
// Supported data types are: string, int, float, bool
|
||||
// Bool does not support the wildcard operator
|
||||
func NewQuery(s string, l Logic) (q *Query, err error) {
|
||||
if l > 2 || l < 0 {
|
||||
l = AND
|
||||
}
|
||||
q = &Query{l: l}
|
||||
if s == "" {
|
||||
err = errors.New("query is empty")
|
||||
return
|
||||
}
|
||||
qm := make(map[string]struct{})
|
||||
fields := strings.Split(s, " ")
|
||||
for _, v := range fields {
|
||||
|
|
@ -83,7 +85,11 @@ func NewQuery(s string, l Logic) (q *Query, err error) {
|
|||
}
|
||||
qm[m[0]] = struct{}{}
|
||||
}
|
||||
q.m = append(q.m, match{strings.Split(m[0], "."), m[1]})
|
||||
q.m = append(q.m, match{
|
||||
strings.Split(m[0], "."),
|
||||
matchFunc(m[1]),
|
||||
boolFunc(m[1]),
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -95,26 +101,23 @@ func (q *Query) check(oks []bool) (b bool) {
|
|||
for _, ok := range oks {
|
||||
if !ok {
|
||||
b = false
|
||||
return
|
||||
}
|
||||
}
|
||||
case OR:
|
||||
b = false
|
||||
case NOT:
|
||||
for _, ok := range oks {
|
||||
if ok {
|
||||
b = false
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (q *Query) match(v reflect.Value) bool {
|
||||
if q.l == OR {
|
||||
return q.orMatch(v)
|
||||
}
|
||||
return q.allMatch(v)
|
||||
}
|
||||
|
||||
func (q *Query) allMatch(v reflect.Value) (ok bool) {
|
||||
func (q *Query) match(v reflect.Value) (ok bool) {
|
||||
if v.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
|
|
@ -122,11 +125,15 @@ func (q *Query) allMatch(v reflect.Value) (ok bool) {
|
|||
if ql == 0 {
|
||||
return
|
||||
}
|
||||
oks := make([]bool, ql)
|
||||
var (
|
||||
oks []bool
|
||||
match bool
|
||||
)
|
||||
if q.l != OR {
|
||||
oks = make([]bool, ql)
|
||||
}
|
||||
for i, qv := range q.m {
|
||||
s := ""
|
||||
fl := len(qv.fields)
|
||||
vl := len(qv.value)
|
||||
if fl == 0 {
|
||||
continue
|
||||
}
|
||||
|
|
@ -134,125 +141,27 @@ func (q *Query) allMatch(v reflect.Value) (ok bool) {
|
|||
for j := 1; j < fl; j++ {
|
||||
val = val.FieldByName(qv.fields[j])
|
||||
}
|
||||
switch val.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16,
|
||||
reflect.Int32, reflect.Int64:
|
||||
s = strconv.Itoa(int(val.Int()))
|
||||
case reflect.String:
|
||||
s = val.String()
|
||||
case reflect.Bool:
|
||||
if vl == 0 {
|
||||
continue
|
||||
}
|
||||
b := val.Bool()
|
||||
if b && qv.value[0] == 't' {
|
||||
oks[i] = true
|
||||
continue
|
||||
}
|
||||
if !b && qv.value[0] == 'f' {
|
||||
oks[i] = true
|
||||
}
|
||||
s, b, err := getValue(val)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
switch s {
|
||||
case "":
|
||||
match = qv.bf(b)
|
||||
default:
|
||||
match = qv.mf(s)
|
||||
}
|
||||
if q.l == OR {
|
||||
if match {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case vl >= 3:
|
||||
if qv.value[0] == any && qv.value[vl-1] == any {
|
||||
if strings.Contains(s, qv.value[1:vl-1]) {
|
||||
oks[i] = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
fallthrough
|
||||
case vl >= 2:
|
||||
switch {
|
||||
case qv.value[0] == any:
|
||||
if strings.HasSuffix(s, qv.value[1:]) {
|
||||
oks[i] = true
|
||||
continue
|
||||
}
|
||||
case qv.value[vl-1] == any:
|
||||
if strings.HasPrefix(s, qv.value[:vl-1]) {
|
||||
oks[i] = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if s == qv.value {
|
||||
oks[i] = true
|
||||
}
|
||||
oks[i] = match
|
||||
}
|
||||
return q.check(oks)
|
||||
}
|
||||
|
||||
func (q *Query) orMatch(v reflect.Value) bool {
|
||||
if v.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
ql := len(q.m)
|
||||
if ql == 0 {
|
||||
return false
|
||||
}
|
||||
for _, qv := range q.m {
|
||||
s := ""
|
||||
fl := len(qv.fields)
|
||||
vl := len(qv.value)
|
||||
if fl == 0 {
|
||||
continue
|
||||
}
|
||||
val := v.FieldByName(qv.fields[0])
|
||||
for j := 1; j < fl; j++ {
|
||||
val = val.FieldByName(qv.fields[j])
|
||||
}
|
||||
switch val.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16,
|
||||
reflect.Int32, reflect.Int64:
|
||||
s = strconv.Itoa(int(val.Int()))
|
||||
case reflect.String:
|
||||
s = val.String()
|
||||
case reflect.Bool:
|
||||
if vl == 0 {
|
||||
continue
|
||||
}
|
||||
b := val.Bool()
|
||||
if b && qv.value[0] == 't' {
|
||||
return true
|
||||
}
|
||||
if !b && qv.value[0] == 'f' {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
default:
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case vl >= 3:
|
||||
if qv.value[0] == any && qv.value[vl-1] == any {
|
||||
if strings.Contains(s, qv.value[1:vl-1]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
fallthrough
|
||||
case vl >= 2:
|
||||
switch {
|
||||
case qv.value[0] == any:
|
||||
if strings.HasSuffix(s, qv.value[1:]) {
|
||||
return true
|
||||
}
|
||||
case qv.value[vl-1] == any:
|
||||
if strings.HasPrefix(s, qv.value[:vl-1]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if s == qv.value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GenericFilter takes any slice of structs value and returns the same slice type containing only the filter matches.
|
||||
// Returns nil if interface{} is not a slice.
|
||||
func (q *Query) GenericFilter(i interface{}) interface{} {
|
||||
|
|
|
|||
|
|
@ -66,9 +66,14 @@ func TestNewQuery(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
const (
|
||||
filter1 = "String:*st* Bool:true"
|
||||
filter2 = "Struct.Struct.Int:2 Struct.Struct.Int:3"
|
||||
)
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
f1 := makefilter(data)
|
||||
q1, err := NewQuery("String:*st* Bool:true", AND)
|
||||
q1, err := NewQuery(filter1, AND)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
|
@ -84,7 +89,7 @@ func TestFilter(t *testing.T) {
|
|||
}
|
||||
|
||||
f2 := makefilter(data)
|
||||
q2, err := NewQuery("Struct.Struct.Int:2 Struct.Struct.Int:3", OR)
|
||||
q2, err := NewQuery(filter2, OR)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
|
@ -100,7 +105,7 @@ func TestFilter(t *testing.T) {
|
|||
}
|
||||
|
||||
f3 := makefilter(data)
|
||||
q3, err := NewQuery("Struct.Struct.Int:2 Struct.Struct.Int:3", NOT)
|
||||
q3, err := NewQuery(filter2, NOT)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
|
@ -114,7 +119,7 @@ func TestFilter(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGenericFilter(t *testing.T) {
|
||||
q1, err := NewQuery("String:*st* Bool:true", AND)
|
||||
q1, err := NewQuery(filter1, AND)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
|
@ -133,7 +138,7 @@ func TestGenericFilter(t *testing.T) {
|
|||
t.Error("f1[1] != data[1]")
|
||||
}
|
||||
|
||||
q2, err := NewQuery("Struct.Struct.Int:2 Struct.Struct.Int:3", OR)
|
||||
q2, err := NewQuery(filter2, OR)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
|
@ -152,7 +157,7 @@ func TestGenericFilter(t *testing.T) {
|
|||
t.Error("f2[1] != data[2]")
|
||||
}
|
||||
|
||||
q3, err := NewQuery("Struct.Struct.Int:2 Struct.Struct.Int:3", NOT)
|
||||
q3, err := NewQuery(filter2, NOT)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
|
@ -169,9 +174,36 @@ func TestGenericFilter(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
const (
|
||||
simple = "Bool:true"
|
||||
comp = "String:*st* Bool:false Struct.Struct.Int:1"
|
||||
compOr = "Int:47 Bool:false Struct.String:s1*"
|
||||
)
|
||||
|
||||
func BenchmarkNewQuerySimple(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
NewQuery(simple, OR)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewQueryComplex(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
NewQuery(comp, AND)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewQueryComplexOr(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
NewQuery(compOr, OR)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFilterSimple(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
q, err := NewQuery("Bool:true", OR)
|
||||
q, err := NewQuery(simple, OR)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
|
@ -183,7 +215,7 @@ func BenchmarkFilterSimple(b *testing.B) {
|
|||
|
||||
func BenchmarkFilterComplex(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
q, err := NewQuery("String:*st* Bool:false Struct.Struct.Int:1", AND)
|
||||
q, err := NewQuery(comp, AND)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
|
@ -195,7 +227,7 @@ func BenchmarkFilterComplex(b *testing.B) {
|
|||
|
||||
func BenchmarkFilterComplexOr(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
q, err := NewQuery("Int:47 Bool:false Struct.String:s1*", OR)
|
||||
q, err := NewQuery(compOr, OR)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
|
@ -207,7 +239,7 @@ func BenchmarkFilterComplexOr(b *testing.B) {
|
|||
|
||||
func BenchmarkGenericFilterSimple(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
q, err := NewQuery("Bool:true", OR)
|
||||
q, err := NewQuery(simple, OR)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
|
@ -218,7 +250,7 @@ func BenchmarkGenericFilterSimple(b *testing.B) {
|
|||
|
||||
func BenchmarkGenericFilterComplex(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
q, err := NewQuery("String:*st* Bool:false Struct.Struct.Int:1", AND)
|
||||
q, err := NewQuery(comp, AND)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
|
@ -229,7 +261,7 @@ func BenchmarkGenericFilterComplex(b *testing.B) {
|
|||
|
||||
func BenchmarkGenericFilterComplexOr(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
q, err := NewQuery("Int:47 Bool:false Struct.String:s1*", OR)
|
||||
q, err := NewQuery(compOr, OR)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
|
|
|
|||
86
helper.go
Normal file
86
helper.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package filter
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
any = '*'
|
||||
anys = string(any)
|
||||
)
|
||||
|
||||
func getValue(v reflect.Value) (s string, b bool, err error) {
|
||||
switch v.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16,
|
||||
reflect.Int32, reflect.Int64:
|
||||
s = strconv.FormatInt(v.Int(), 10)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
s = strconv.FormatFloat(v.Float(), 'f', -1, 64)
|
||||
case reflect.String:
|
||||
s = v.String()
|
||||
case reflect.Bool:
|
||||
b = v.Bool()
|
||||
default:
|
||||
err = errors.New("nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func matchFunc(v string) func(string) bool {
|
||||
vl := len(v)
|
||||
switch {
|
||||
case vl >= 3:
|
||||
if v[0] == any && v[vl-1] == any && v[1] != any && v[vl-2] != any {
|
||||
return func(s string) bool {
|
||||
return strings.Contains(s, v[1:vl-1])
|
||||
}
|
||||
}
|
||||
fallthrough
|
||||
case vl >= 2:
|
||||
switch {
|
||||
case v[0] == any && v[1] != any:
|
||||
return func(s string) bool {
|
||||
return strings.HasSuffix(s, v[1:])
|
||||
}
|
||||
case v[vl-1] == any && v[vl-2] != any:
|
||||
return func(s string) bool {
|
||||
return strings.HasPrefix(s, v[:vl-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
v = strings.Replace(v, anys+anys, anys, -1)
|
||||
return func(s string) bool {
|
||||
if s == v {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
func boolFunc(v string) func(bool) bool {
|
||||
f := func(b bool) bool {
|
||||
return false
|
||||
}
|
||||
if len(v) == 0 {
|
||||
return f
|
||||
}
|
||||
if v[0] == 't' {
|
||||
return func(b bool) bool {
|
||||
if b {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
if v[0] == 'f' {
|
||||
return func(b bool) bool {
|
||||
if !b {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue