mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-09-07 17:15:56 +00:00
initial impl of like
This commit is contained in:
parent
7f1b1f63ef
commit
9eb8fcb37f
2 changed files with 343 additions and 0 deletions
159
like.go
Normal file
159
like.go
Normal 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
184
like_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue