initial impl of like

This commit is contained in:
Jaden Weiss 2020-07-06 14:50:52 -04:00
parent 7f1b1f63ef
commit 9eb8fcb37f
No known key found for this signature in database
GPG key ID: 177F065773634B67
2 changed files with 343 additions and 0 deletions

159
like.go Normal file
View file

@ -0,0 +1,159 @@
package pilosa
import (
"strings"
"unicode/utf8"
)
func tokenizeLike(like string) []string {
var tokens []string
for like != "" {
var token string
i := strings.IndexAny(like, "%_")
switch {
case i == 0:
j := 1
for j < len(like) && (like[j] == '%' || like[j] == '_') {
j++
}
token, like = like[:j], like[j:]
case i < 0:
token, like = like, ""
default:
token, like = like[:i], like[i:]
}
tokens = append(tokens, token)
}
return tokens
}
type filterStepKind uint8
const (
filterStepPrefix filterStepKind = iota
filterStepSkipN
filterStepSkipThrough
filterStepMinLength
)
type filterStep struct {
kind filterStepKind
str string
n int
}
func planLike(like string) []filterStep {
tokens := tokenizeLike(like)
steps := make([]filterStep, 0, len(tokens))
var merged bool
for i, t := range tokens {
if merged {
merged = false
continue
}
var step filterStep
hasPercent := strings.ContainsRune(t, '%')
underscores := strings.Count(t, "_")
switch {
case hasPercent && i+1 < len(tokens):
step = filterStep{
kind: filterStepSkipThrough,
str: tokens[i+1],
n: underscores,
}
merged = true
case hasPercent:
step = filterStep{
kind: filterStepMinLength,
n: underscores,
}
case underscores > 0:
step = filterStep{
kind: filterStepSkipN,
n: underscores,
}
default:
step = filterStep{
kind: filterStepPrefix,
str: t,
}
}
steps = append(steps, step)
}
return steps
}
func matchLike(key string, like ...filterStep) bool {
for i, step := range like {
switch step.kind {
case filterStepPrefix:
if !strings.HasPrefix(key, step.str) {
return false
}
key = key[len(step.str):]
case filterStepSkipN:
n := step.n
for j := 0; j < n; j++ {
_, len := utf8.DecodeRuneInString(key)
if len == 0 {
return false
}
key = key[len:]
}
case filterStepSkipThrough:
var skipped int
for skipped < step.n {
j := strings.Index(key, step.str)
switch j {
case -1:
return false
case 0:
_, len := utf8.DecodeRuneInString(key)
if len == 0 {
return false
}
key = key[len:]
skipped += len
default:
k := -1
for k = range key[:j] {
}
skipped += k + 1
key = key[j:]
}
}
remaining := like[i+1:]
for {
j := strings.Index(key, step.str)
switch {
case j == -1:
return false
case j > 0:
key = key[j:]
}
if matchLike(key[len(step.str):], remaining...) {
return true
}
key = key[1:]
}
case filterStepMinLength:
if len(key) < step.n {
return false
}
j := -1
for j = range key {
}
return j+1 >= step.n
default:
panic("invalid step")
}
}
return key == ""
}

184
like_test.go Normal file
View file

@ -0,0 +1,184 @@
package pilosa
import (
"reflect"
"testing"
)
func TestPlanLike(t *testing.T) {
t.Parallel()
cases := []struct {
name string
like string
plan []filterStep
match, nonmatch []string
}{
{
name: "Empty",
like: "",
plan: []filterStep{},
match: []string{""},
nonmatch: []string{"a", " "},
},
{
name: "Exact",
like: "x",
plan: []filterStep{
{
kind: filterStepPrefix,
str: "x",
},
},
match: []string{"x"},
nonmatch: []string{"", "y", "z", "xy", "yx"},
},
{
name: "Anything",
like: "%",
plan: []filterStep{
{
kind: filterStepMinLength,
n: 0,
},
},
match: []string{"", "a", "b", "ab"},
},
{
name: "Prefix",
like: "x%",
plan: []filterStep{
{
kind: filterStepPrefix,
str: "x",
},
{
kind: filterStepMinLength,
n: 0,
},
},
match: []string{"xy", "xyz", "xyzzy"},
nonmatch: []string{"plugh", "yx", ""},
},
{
name: "Suffix",
like: "%x",
plan: []filterStep{
{
kind: filterStepSkipThrough,
str: "x",
},
},
match: []string{"x", "xx", "ax"},
nonmatch: []string{"", "a", "x^"},
},
{
name: "Sandwich",
like: "x%y",
plan: []filterStep{
{
kind: filterStepPrefix,
str: "x",
},
{
kind: filterStepSkipThrough,
str: "y",
},
},
match: []string{"xy", "xzy", "xyzzy"},
nonmatch: []string{"plugh", ".xy.", ".x.y", "x.y."},
},
{
name: "SingleRune",
like: "_",
plan: []filterStep{
{
kind: filterStepSkipN,
n: 1,
},
},
match: []string{"a", "á", "☺"},
nonmatch: []string{"ab", "á", "h̷"},
},
{
name: "DoubleRune",
like: "__",
plan: []filterStep{
{
kind: filterStepSkipN,
n: 2,
},
},
match: []string{"ab", "á", "h̷"},
nonmatch: []string{"a", "á", "☺", "abc"},
},
{
name: "MiddleBlank",
like: "x_y",
plan: []filterStep{
{
kind: filterStepPrefix,
str: "x",
},
{
kind: filterStepSkipN,
n: 1,
},
{
kind: filterStepPrefix,
str: "y",
},
},
match: []string{"x.y", "xay", "x y", "x⊕y"},
nonmatch: []string{"x++y", "", "a"},
},
{
name: "MinLength",
like: "_%_",
plan: []filterStep{
{
kind: filterStepMinLength,
n: 2,
},
},
match: []string{"ab", "á", "abc", "pilosa"},
nonmatch: []string{"h", "á", ".", "☺"},
},
}
t.Run("Plan", func(t *testing.T) {
t.Parallel()
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
plan := planLike(c.like)
if !reflect.DeepEqual(plan, c.plan) {
t.Errorf("incorrect plan: %v", plan)
}
})
}
})
t.Run("Match", func(t *testing.T) {
t.Parallel()
for _, c := range cases {
c := c
t.Run(c.name, func(t *testing.T) {
t.Parallel()
for _, m := range c.match {
if !matchLike(m, c.plan...) {
t.Errorf("key %q was not matched", m)
}
}
for _, nm := range c.nonmatch {
if matchLike(nm, c.plan...) {
t.Errorf("key %q was matched", nm)
}
}
})
}
})
}