From 0ebcd6967bf3914f6a0282cf656c558e6179162e Mon Sep 17 00:00:00 2001 From: ston1th Date: Sun, 9 Jul 2017 20:04:05 +0200 Subject: [PATCH] added float support and cleaner code --- README.md | 60 ++++++++++++------ cmd/main.go | 2 +- filter.go | 167 +++++++++++-------------------------------------- filter_test.go | 56 +++++++++++++---- helper.go | 86 +++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 161 deletions(-) create mode 100644 helper.go diff --git a/README.md b/README.md index 334ef6f..ecec3dd 100644 --- a/README.md +++ b/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 ``` diff --git a/cmd/main.go b/cmd/main.go index 2e9d9a9..572b144 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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() { diff --git a/filter.go b/filter.go index 6c9f8d0..983334f 100644 --- a/filter.go +++ b/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: : -// Wildcard match: +// Wildcard match (use ** to match the '*' character): // :* // :* // :** @@ -58,13 +56,17 @@ type Query struct { // .: // Multiple matches: // : :* -// 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{} { diff --git a/filter_test.go b/filter_test.go index c779075..0290bed 100644 --- a/filter_test.go +++ b/filter_test.go @@ -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) } diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..bf8753b --- /dev/null +++ b/helper.go @@ -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 +}