use regex implementing wildcard

This commit is contained in:
finley
2022-07-08 23:48:42 +08:00
parent 9e155a617c
commit 98f986a363
3 changed files with 104 additions and 121 deletions

View File

@@ -280,7 +280,10 @@ func execPersist(db *DB, args [][]byte) redis.Reply {
// execKeys returns all keys matching the given pattern
func execKeys(db *DB, args [][]byte) redis.Reply {
pattern := wildcard.CompilePattern(string(args[0]))
pattern, err := wildcard.CompilePattern(string(args[0]))
if err != nil {
return protocol.MakeErrReply("ERR illegal wildcard")
}
result := make([][]byte, 0)
db.data.ForEach(func(key string, val interface{}) bool {
if pattern.IsMatch(key) {

View File

@@ -1,131 +1,75 @@
package wildcard
const (
normal = iota
all // *
any // ?
setSymbol // []
rangSymbol // [a-b]
negSymbol // [^a]
import (
"errors"
"regexp"
"strings"
)
type item struct {
character byte
set map[byte]bool
typeCode int
}
func (i *item) contains(c byte) bool {
if i.typeCode == setSymbol {
_, ok := i.set[c]
return ok
} else if i.typeCode == rangSymbol {
if _, ok := i.set[c]; ok {
return true
}
var (
min uint8 = 255
max uint8 = 0
)
for k := range i.set {
if min > k {
min = k
}
if max < k {
max = k
}
}
return c >= min && c <= max
} else {
_, ok := i.set[c]
return !ok
}
}
// Pattern represents a wildcard pattern
type Pattern struct {
items []*item
exp *regexp.Regexp
}
var replaceMap = map[byte]string{
// characters in the wildcard that must be escaped in the regexp
'+': `\+`,
')': `\)`,
'$': `\$`,
'.': `\.`,
'{': `\{`,
'}': `\}`,
'|': `\|`,
'*': ".*",
'?': ".",
}
// CompilePattern convert wildcard string to Pattern
func CompilePattern(src string) *Pattern {
items := make([]*item, 0)
escape := false
inSet := false
var set map[byte]bool
for _, v := range src {
c := byte(v)
if escape {
items = append(items, &item{typeCode: normal, character: c})
escape = false
} else if c == '*' {
items = append(items, &item{typeCode: all})
} else if c == '?' {
items = append(items, &item{typeCode: any})
} else if c == '\\' {
escape = true
} else if c == '[' {
if !inSet {
inSet = true
set = make(map[byte]bool)
} else {
set[c] = true
func CompilePattern(src string) (*Pattern, error) {
regexSrc := strings.Builder{}
regexSrc.WriteByte('^')
for i := 0; i < len(src); i++ {
ch := src[i]
if ch == '\\' {
if i == len(src)-1 {
return nil, errors.New("end with escape \\")
}
} else if c == ']' {
if inSet {
inSet = false
typeCode := setSymbol
if _, ok := set['-']; ok {
typeCode = rangSymbol
delete(set, '-')
regexSrc.WriteByte(ch)
regexSrc.WriteByte(src[i+1])
i++ // skip escaped character
} else if ch == '^' {
if i == 0 {
regexSrc.WriteString(`\^`)
} else if i == 1 {
if src[i-1] == '[' {
regexSrc.WriteString(`^`) // src is: [^
} else {
regexSrc.WriteString(`\^`)
}
if _, ok := set['^']; ok {
typeCode = negSymbol
delete(set, '^')
}
items = append(items, &item{typeCode: typeCode, set: set})
} else {
items = append(items, &item{typeCode: normal, character: c})
if src[i-1] == '[' && src[i-2] != '\\' {
regexSrc.WriteString(`^`) // src is: [^, except \[^
} else {
regexSrc.WriteString(`\^`)
}
}
} else if escaped, toEscape := replaceMap[ch]; toEscape {
regexSrc.WriteString(escaped)
} else {
if inSet {
set[c] = true
} else {
items = append(items, &item{typeCode: normal, character: c})
}
regexSrc.WriteByte(ch)
}
}
return &Pattern{
items: items,
regexSrc.WriteByte('$')
re, err := regexp.Compile(regexSrc.String())
if err != nil {
return nil, err
}
return &Pattern{
exp: re,
}, nil
}
// IsMatch returns whether the given string matches pattern
func (p *Pattern) IsMatch(s string) bool {
if len(p.items) == 0 {
return len(s) == 0
}
m := len(s)
n := len(p.items)
table := make([][]bool, m+1)
for i := 0; i < m+1; i++ {
table[i] = make([]bool, n+1)
}
table[0][0] = true
for j := 1; j < n+1; j++ {
table[0][j] = table[0][j-1] && p.items[j-1].typeCode == all
}
for i := 1; i < m+1; i++ {
for j := 1; j < n+1; j++ {
if p.items[j-1].typeCode == all {
table[i][j] = table[i-1][j] || table[i][j-1]
} else {
table[i][j] = table[i-1][j-1] &&
(p.items[j-1].typeCode == any ||
(p.items[j-1].typeCode == normal && uint8(s[i-1]) == p.items[j-1].character) ||
(p.items[j-1].typeCode >= setSymbol && p.items[j-1].contains(s[i-1])))
}
}
}
return table[m][n]
return p.exp.Match([]byte(s))
}

View File

@@ -3,11 +3,19 @@ package wildcard
import "testing"
func TestWildCard(t *testing.T) {
p := CompilePattern("")
p, err := CompilePattern("")
if err != nil {
t.Error(err)
return
}
if !p.IsMatch("") {
t.Error("expect true actually false")
}
p = CompilePattern("a")
p, err = CompilePattern("a")
if err != nil {
t.Error(err)
return
}
if !p.IsMatch("a") {
t.Error("expect true actually false")
}
@@ -16,7 +24,11 @@ func TestWildCard(t *testing.T) {
}
// test '?'
p = CompilePattern("a?")
p, err = CompilePattern("a?")
if err != nil {
t.Error(err)
return
}
if !p.IsMatch("ab") {
t.Error("expect true actually false")
}
@@ -31,7 +43,11 @@ func TestWildCard(t *testing.T) {
}
// test *
p = CompilePattern("a*")
p, err = CompilePattern("a*")
if err != nil {
t.Error(err)
return
}
if !p.IsMatch("ab") {
t.Error("expect true actually false")
}
@@ -46,7 +62,11 @@ func TestWildCard(t *testing.T) {
}
// test []
p = CompilePattern("a[ab[]")
p, err = CompilePattern("a[ab[]")
if err != nil {
t.Error(err)
return
}
if !p.IsMatch("ab") {
t.Error("expect true actually false")
}
@@ -64,7 +84,11 @@ func TestWildCard(t *testing.T) {
}
// test [a-c]
p = CompilePattern("h[a-c]llo")
p, err = CompilePattern("h[a-c]llo")
if err != nil {
t.Error(err)
return
}
if !p.IsMatch("hallo") {
t.Error("expect true actually false")
}
@@ -81,8 +105,12 @@ func TestWildCard(t *testing.T) {
t.Error("expect false actually true")
}
// test [^]
p = CompilePattern("h[^ab]llo")
//test [^]
p, err = CompilePattern("h[^ab]llo")
if err != nil {
t.Error(err)
return
}
if p.IsMatch("hallo") {
t.Error("expect false actually true")
}
@@ -94,12 +122,20 @@ func TestWildCard(t *testing.T) {
}
// test escape
p = CompilePattern("\\\\") // pattern: \\
if !p.IsMatch("\\") {
p, err = CompilePattern(`\\\\`)
if err != nil {
t.Error(err)
return
}
if !p.IsMatch(`\\`) {
t.Error("expect true actually false")
}
p = CompilePattern("\\*")
p, err = CompilePattern("\\*")
if err != nil {
t.Error(err)
return
}
if !p.IsMatch("*") {
t.Error("expect true actually false")
}