commit e309b478f526c410d0baefc8b063fa22820b296f Author: ston1th Date: Sun Jul 9 01:05:58 2017 +0200 initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2bb060c --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (C) 2017 Marius Schellenberger +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The names of the authors and/or contributors may not be used to + endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ston1th BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f3738f9 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# filter - a simple struct field filter for go + +## Usage + +``` +// Example data structure +type Example struct { + String string + Int int + Bool bool + Struct Struct1 +} + +type Struct1 struct { + String string + Struct Struct2 +} + +type Struct2 struct { + Int int +} + +// Filter implements the filter.Filter interface +type Filter []Example + +func (v Filter) Len() int { return len(v) } +func (v Filter) Index(i int) interface{} { return v[i] } +func (v *Filter) Remove(i, j int) { *v = append((*v)[:i], (*v)[j:]...) } + +// Example data +var data = Filter{ + {"str1", 42, true, Struct1{"s1", Struct2{1}}}, + {"2str", 42, true, Struct1{"s2", Struct2{2}}}, + {"str3", 42, false, Struct1{"s3", Struct2{3}}}, +} +``` + +### Explicit match + +``` +filter.NewQuery("Bool:true").Filter(&data) + +data = [{str1 42 true {s1 {1}}} {str2 42 true {s2 {2}}}] +``` + +### Wildcard match + +``` +filter.NewQuery("String:s*").Filter(&data) + +data = [{str1 42 true {s1 {1}}} {str3 42 false {s3 {3}}}] +``` + +### Nested struct match + +``` +filter.NewQuery("Struct.Struct.Int:3").Filter(&data) + +data = [{str3 42 false {s3 {3}}}] +``` + +### Multiple matches + +``` +filter.NewQuery("Struct.String:*2 Bool:true").Filter(&data) + +data = [{2str 42 true {s2 {2}}}] +``` + +## Benchmarks + +``` +go test -cpu 1,2,4 --bench . +BenchmarkFilterSimple 2000000 899 ns/op 432 B/op 12 allocs/op +BenchmarkFilterSimple-2 2000000 896 ns/op 432 B/op 12 allocs/op +BenchmarkFilterSimple-4 1000000 1439 ns/op 432 B/op 12 allocs/op +BenchmarkGenericFilterSimple 500000 2438 ns/op 1200 B/op 24 allocs/op +BenchmarkGenericFilterSimple-2 500000 2406 ns/op 1200 B/op 24 allocs/op +BenchmarkGenericFilterSimple-4 500000 2645 ns/op 1200 B/op 24 allocs/op +BenchmarkFilterComplex 500000 2929 ns/op 672 B/op 42 allocs/op +BenchmarkFilterComplex-2 500000 2918 ns/op 672 B/op 42 allocs/op +BenchmarkFilterComplex-4 500000 5574 ns/op 672 B/op 42 allocs/op +BenchmarkGenericFilterComplex 200000 7343 ns/op 1680 B/op 84 allocs/op +BenchmarkGenericFilterComplex-2 200000 7400 ns/op 1680 B/op 84 allocs/op +BenchmarkGenericFilterComplex-4 200000 11979 ns/op 1680 B/op 84 allocs/op +``` diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..ea88442 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,52 @@ +// Copyright (C) 2017 Marius Schellenberger + +package main + +import ( + "fmt" + "git.giftfish.de/ston1th/filter" +) + +// Example data structure +type Example struct { + String string + Int int + Bool bool + Struct Struct1 +} + +type Struct1 struct { + String string + Struct Struct2 +} + +type Struct2 struct { + Int int +} + +// Filter implements the filter.Filter interface +type Filter []Example + +func (v Filter) Len() int { return len(v) } +func (v Filter) Index(i int) interface{} { return v[i] } +func (v *Filter) Remove(i, j int) { *v = append((*v)[:i], (*v)[j:]...) } + +// Example data +var data = Filter{ + {"str1", 42, true, Struct1{"s1", Struct2{1}}}, + {"2str", 42, true, Struct1{"s2", Struct2{2}}}, + {"str3", 42, false, Struct1{"s3", Struct2{3}}}, +} + +func main() { + f := filter.NewQuery("String:s*").GenericFilter(data) + if f != nil { + for _, v := range f.(Filter) { + fmt.Println("Generic:", v) + } + } + filter.NewQuery("String:s*").Filter(&data) + for _, v := range data { + fmt.Println("Interface:", v) + } +} diff --git a/filter.go b/filter.go new file mode 100644 index 0000000..8647ecf --- /dev/null +++ b/filter.go @@ -0,0 +1,152 @@ +// Copyright (C) 2017 Marius Schellenberger + +// Package filter implements simple struct field filtering +package filter + +import ( + "reflect" + "strconv" + "strings" +) + +const any = '*' + +type Filter interface { + Len() int + Index(int) interface{} + Remove(int, int) +} + +type match struct { + fields []string + value string +} + +// Query implements the filter query +type Query []match + +// NewQuery parses the filter query. +// Examples: +// Explicit match: : +// Wildcard match: +// :* +// :* +// :** +// Nested structs: +// .: +// Multiple matches: +// : :* +// Supported data types are: string, int, bool +// Bool does not support the wildcard operator +func NewQuery(s string) (q Query) { + fields := strings.Split(s, " ") + for _, v := range fields { + m := strings.Split(v, ":") + if len(m) == 2 { + q = append(q, match{strings.Split(m[0], "."), m[1]}) + } + } + return +} + +func (q Query) match(v reflect.Value) (ok bool) { + if v.Kind() != reflect.Struct { + return + } + if len(q) == 0 { + return + } + ok = true + for _, qv := range q { + s := "" + fl := len(qv.fields) + if fl == 0 { + continue + } + val := v.FieldByName(qv.fields[0]) + for i := 1; i < fl; i++ { + val = val.FieldByName(qv.fields[i]) + } + 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 val.Bool() { + s = "true" + } else { + s = "false" + } + if s != qv.value { + ok = false + } + continue + default: + ok = false + continue + } + vl := len(qv.value) + switch { + case vl >= 3: + if qv.value[0] == any && qv.value[vl-1] == any { + if strings.Contains(s, qv.value[1:vl-1]) { + continue + } + } + fallthrough + case vl >= 2: + if qv.value[0] == any { + if strings.HasSuffix(s, qv.value[1:]) { + continue + } + } + if qv.value[vl-1] == any { + if strings.HasPrefix(s, qv.value[:vl-1]) { + continue + } + } + } + if s != qv.value { + ok = false + } + } + return +} + +// 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{} { + v := reflect.ValueOf(i) + if v.Kind() != reflect.Slice { + return nil + } + ret := reflect.MakeSlice(v.Type(), 0, 0) + for i := 0; i < v.Len(); i++ { + val := v.Index(i) + if q.match(val) { + ret = reflect.Append(ret, val) + } + } + return ret.Interface() +} + +func (q Query) Filter(f Filter) { + count := 0 + i := 0 + for ; i < f.Len(); i++ { + if q.match(reflect.ValueOf(f.Index(i))) { + if count > 0 { + f.Remove(i-count, i) + i -= count + } + count = 0 + } else { + count++ + } + } + if count > 0 { + f.Remove(i-count, i) + } +} diff --git a/filter_test.go b/filter_test.go new file mode 100644 index 0000000..dfad7e1 --- /dev/null +++ b/filter_test.go @@ -0,0 +1,139 @@ +// Copyright (C) 2017 Marius Schellenberger + +package filter + +import "testing" + +type Example struct { + String string + Int int + Bool bool + Struct Struct1 +} + +type Struct1 struct { + String string + Struct Struct2 +} + +type Struct2 struct { + Int int +} + +type filter []Example + +func makefilter(v filter) (f filter) { + f = make(filter, len(v)) + copy(f, v) + return +} + +func (v filter) Len() int { return len(v) } +func (v filter) Index(i int) interface{} { return v[i] } +func (v *filter) Remove(i, j int) { *v = append((*v)[:i], (*v)[j:]...) } + +var ( + data = filter{ + {"str1", 42, true, Struct1{"s1", Struct2{1}}}, + {"2str", 42, true, Struct1{"s2", Struct2{2}}}, + {"str3", 42, false, Struct1{"s3", Struct2{3}}}, + } + benchData = filter{ + {"str1", 41, true, Struct1{"s1", Struct2{1}}}, + {"2str", 42, true, Struct1{"s2", Struct2{1}}}, + {"str3", 43, false, Struct1{"s3", Struct2{1}}}, + {"str4", 44, false, Struct1{"s4", Struct2{1}}}, + {"str5", 45, false, Struct1{"s5", Struct2{1}}}, + {"str6", 46, false, Struct1{"s6", Struct2{1}}}, + {"str7", 47, true, Struct1{"s7", Struct2{1}}}, + {"str8", 48, true, Struct1{"s8", Struct2{1}}}, + {"str9", 49, true, Struct1{"s9", Struct2{1}}}, + {"str10", 50, true, Struct1{"s10", Struct2{1}}}, + {"str11", 51, false, Struct1{"s11", Struct2{1}}}, + {"str12", 52, false, Struct1{"s12", Struct2{1}}}, + } +) + +func TestFilter(t *testing.T) { + f1 := makefilter(data) + NewQuery("String:*st* Bool:true").Filter(&f1) + if len(f1) != 2 { + t.Error("f1 len is:", len(f1)) + } + if f1[0] != data[0] { + t.Error("f1[0] != data[0]") + } + if f1[1] != data[1] { + t.Error("f1[1] != data[1]") + } + f2 := makefilter(data) + NewQuery("Struct.Struct.Int:3").Filter(&f2) + if len(f2) != 1 { + t.Error("f2 len is:", len(f2)) + } + if f2[0] != data[2] { + t.Error("f1[0] != data[2]") + } +} + +func TestGenericFilter(t *testing.T) { + i1 := NewQuery("String:*st* Bool:true").GenericFilter(data) + if i1 == nil { + t.Error("i1 is nil") + } + f1 := i1.(filter) + if len(f1) != 2 { + t.Error("f1 len is:", len(f1)) + } + if f1[0] != data[0] { + t.Error("f1[0] != data[0]") + } + if f1[1] != data[1] { + t.Error("f1[1] != data[1]") + } + i2 := NewQuery("Struct.Struct.Int:3").GenericFilter(data) + if i2 == nil { + t.Error("i2 is nil") + } + f2 := i2.(filter) + if len(f2) != 1 { + t.Error("f2 len is:", len(f2)) + } + if f2[0] != data[2] { + t.Error("f1[0] != data[2]") + } +} + +func BenchmarkFilterSimple(b *testing.B) { + b.ReportAllocs() + q := NewQuery("Bool:true") + fd := makefilter(benchData) + for i := 0; i < b.N; i++ { + q.Filter(&fd) + } +} + +func BenchmarkGenericFilterSimple(b *testing.B) { + b.ReportAllocs() + q := NewQuery("Bool:true") + for i := 0; i < b.N; i++ { + q.GenericFilter(benchData) + } +} + +func BenchmarkFilterComplex(b *testing.B) { + b.ReportAllocs() + q := NewQuery("String:*st* Bool:false Struct.Struct.Int:1") + fd := makefilter(benchData) + for i := 0; i < b.N; i++ { + q.Filter(&fd) + } +} + +func BenchmarkGenericFilterComplex(b *testing.B) { + b.ReportAllocs() + q := NewQuery("String:*st* Bool:false Struct.Struct.Int:1") + for i := 0; i < b.N; i++ { + q.GenericFilter(benchData) + } +}