diff --git a/src/pkg/filters/filters.go b/src/pkg/filters/filters.go new file mode 100644 index 000000000..0d62199e6 --- /dev/null +++ b/src/pkg/filters/filters.go @@ -0,0 +1,139 @@ +package filters + +import "strings" + +type comparator int + +const ( + UnknownComparator comparator = iota + // a == b + Equal + // a > b + Greater + // a < b + Less + // a < b < c + Between + // "foo" contains "f" + Contains + // "f" is found in "foo" + In +) + +const delimiter = "," + +func join(s ...string) string { + return strings.Join(s, delimiter) +} + +func split(s string) []string { + return strings.Split(s, delimiter) +} + +func norm(s string) string { + return strings.ToLower(s) +} + +// Filter contains a comparator func and the target to +// compare values against. Filter.Matches(v) returns +// true if Filter.Comparer(filter.target, v) is true. +type Filter struct { + Comparator comparator `json:"comparator"` + Category any `json:"category"` // a caller-provided identifier. Probably an iota or string const. + Target string `json:"target"` // the value to compare against + Negate bool `json:"negate"` // when true, negate the comparator result +} + +// NewEquals creates a filter which Matches(v) is true if +// target == v +func NewEquals(negate bool, category any, target string) Filter { + return Filter{Equal, category, norm(target), negate} +} + +// NewGreater creates a filter which Matches(v) is true if +// target > v +func NewGreater(negate bool, category any, target string) Filter { + return Filter{Greater, category, norm(target), negate} +} + +// NewLess creates a filter which Matches(v) is true if +// target < v +func NewLess(negate bool, category any, target string) Filter { + return Filter{Less, category, norm(target), negate} +} + +// NewBetween creates a filter which Matches(v) is true if +// lesser < v && v < greater +func NewBetween(negate bool, category any, lesser, greater string) Filter { + return Filter{Between, category, norm(join(lesser, greater)), negate} +} + +// NewContains creates a filter which Matches(v) is true if +// super.Contains(v) +func NewContains(negate bool, category any, super string) Filter { + return Filter{Contains, category, norm(super), negate} +} + +// NewIn creates a filter which Matches(v) is true if +// v.Contains(substr) +func NewIn(negate bool, category any, substr string) Filter { + return Filter{In, category, norm(substr), negate} +} + +// Checks whether the filter matches the input +func (f Filter) Matches(input string) bool { + var cmp func(string, string) bool + switch f.Comparator { + case Equal: + cmp = equals + case Greater: + cmp = greater + case Less: + cmp = less + case Between: + cmp = between + case Contains: + cmp = contains + case In: + cmp = in + } + result := cmp(f.Target, norm(input)) + if f.Negate { + result = !result + } + return result +} + +// true if t == i +func equals(target, input string) bool { + return target == input +} + +// true if t > i +func greater(target, input string) bool { + return target > input +} + +// true if t < i +func less(target, input string) bool { + return target < input +} + +// assumes target is a delimited string. +// true if both: +// - less(target[0], input) +// - greater(target[1], input) +func between(target, input string) bool { + parts := split(target) + return less(parts[0], input) && greater(parts[1], input) +} + +// true if target contains input as a substring. +func contains(target, input string) bool { + return strings.Contains(target, input) +} + +// true if input contains target as a substring. +func in(target, input string) bool { + return strings.Contains(input, target) +} diff --git a/src/pkg/filters/filters_test.go b/src/pkg/filters/filters_test.go new file mode 100644 index 000000000..7512a1f8d --- /dev/null +++ b/src/pkg/filters/filters_test.go @@ -0,0 +1,147 @@ +package filters_test + +import ( + "testing" + + "github.com/alcionai/corso/pkg/filters" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +type FiltersSuite struct { + suite.Suite +} + +func TestFiltersSuite(t *testing.T) { + suite.Run(t, new(FiltersSuite)) +} + +func (suite *FiltersSuite) TestEquals() { + make := filters.NewEquals + f := make(false, "", "foo") + nf := make(true, "", "foo") + + table := []struct { + input string + expectF assert.BoolAssertionFunc + expectNF assert.BoolAssertionFunc + }{ + {"foo", assert.True, assert.False}, + {"bar", assert.False, assert.True}, + } + for _, test := range table { + suite.T().Run(test.input, func(t *testing.T) { + test.expectF(t, f.Matches(test.input), "filter") + test.expectNF(t, nf.Matches(test.input), "negated filter") + }) + } +} + +func (suite *FiltersSuite) TestGreater() { + make := filters.NewGreater + f := make(false, "", "5") + nf := make(true, "", "5") + + table := []struct { + input string + expectF assert.BoolAssertionFunc + expectNF assert.BoolAssertionFunc + }{ + {"4", assert.True, assert.False}, + {"5", assert.False, assert.True}, + {"6", assert.False, assert.True}, + } + for _, test := range table { + suite.T().Run(test.input, func(t *testing.T) { + test.expectF(t, f.Matches(test.input), "filter") + test.expectNF(t, nf.Matches(test.input), "negated filter") + }) + } +} + +func (suite *FiltersSuite) TestLess() { + make := filters.NewLess + f := make(false, "", "5") + nf := make(true, "", "5") + + table := []struct { + input string + expectF assert.BoolAssertionFunc + expectNF assert.BoolAssertionFunc + }{ + {"6", assert.True, assert.False}, + {"5", assert.False, assert.True}, + {"4", assert.False, assert.True}, + } + for _, test := range table { + suite.T().Run(test.input, func(t *testing.T) { + test.expectF(t, f.Matches(test.input), "filter") + test.expectNF(t, nf.Matches(test.input), "negated filter") + }) + } +} + +func (suite *FiltersSuite) TestBetween() { + make := filters.NewBetween + f := make(false, "", "abc", "def") + nf := make(true, "", "abc", "def") + + table := []struct { + input string + expectF assert.BoolAssertionFunc + expectNF assert.BoolAssertionFunc + }{ + {"cd", assert.True, assert.False}, + {"a", assert.False, assert.True}, + {"1", assert.False, assert.True}, + {"f", assert.False, assert.True}, + } + for _, test := range table { + suite.T().Run(test.input, func(t *testing.T) { + test.expectF(t, f.Matches(test.input), "filter") + test.expectNF(t, nf.Matches(test.input), "negated filter") + }) + } +} + +func (suite *FiltersSuite) TestContains() { + make := filters.NewContains + f := make(false, "", "smurfs") + nf := make(true, "", "smurfs") + + table := []struct { + input string + expectF assert.BoolAssertionFunc + expectNF assert.BoolAssertionFunc + }{ + {"murf", assert.True, assert.False}, + {"frum", assert.False, assert.True}, + } + for _, test := range table { + suite.T().Run(test.input, func(t *testing.T) { + test.expectF(t, f.Matches(test.input), "filter") + test.expectNF(t, nf.Matches(test.input), "negated filter") + }) + } +} + +func (suite *FiltersSuite) TestIn() { + make := filters.NewIn + f := make(false, "", "murf") + nf := make(true, "", "murf") + + table := []struct { + input string + expectF assert.BoolAssertionFunc + expectNF assert.BoolAssertionFunc + }{ + {"smurfs", assert.True, assert.False}, + {"sfrums", assert.False, assert.True}, + } + for _, test := range table { + suite.T().Run(test.input, func(t *testing.T) { + test.expectF(t, f.Matches(test.input), "filter") + test.expectNF(t, nf.Matches(test.input), "negated filter") + }) + } +}