add pattern matching

This commit is contained in:
Aleksandr Trushkin
2024-02-07 16:33:44 +03:00
parent 2f4a973b45
commit b8ce09f7ec
3 changed files with 327 additions and 8 deletions

164
internal/matcher/radix.go Normal file
View File

@ -0,0 +1,164 @@
package matcher
import (
"regexp"
"strings"
)
const radixWildcard = '*'
type radixNode struct {
exact bool
next map[rune]*radixNode
}
type radixMatcher struct {
root *radixNode
caseInsensitive bool
saved []string
regexps []*regexp.Regexp
}
type RadixOpt func(m *radixMatcher)
func RadixCaseInsensitive() RadixOpt {
return func(m *radixMatcher) {
m.caseInsensitive = true
}
}
func NewRadix(opts ...RadixOpt) *radixMatcher {
m := &radixMatcher{
root: &radixNode{
next: make(map[rune]*radixNode),
},
}
for _, opt := range opts {
opt(m)
}
return m
}
func (r *radixMatcher) MatchByPattern(value string) (pattern string) {
originValue := value
if r.caseInsensitive {
value = strings.ToLower(value)
}
node := r.root
lastIdx := len([]rune(value)) - 1
var sb strings.Builder
for i, v := range value {
var ok bool
if _, ok := node.next[radixWildcard]; ok {
_, _ = sb.WriteRune(radixWildcard)
return sb.String()
}
node, ok = node.next[v]
if !ok {
return r.findByRegexp(originValue)
}
sb.WriteRune(v)
if i != lastIdx {
continue
}
if !node.exact {
return r.findByRegexp(value)
}
return sb.String()
}
return r.findByRegexp(originValue)
}
func (r *radixMatcher) Match(value string) bool {
originValue := value
if r.caseInsensitive {
value = strings.ToLower(value)
}
node := r.root
lastIdx := len([]rune(value)) - 1
for i, v := range value {
var ok bool
if _, ok = node.next[radixWildcard]; ok {
return true
}
node, ok = node.next[v]
if !ok {
return r.findByRegexp(originValue) != ""
}
if i == lastIdx {
return node.exact
}
}
return r.findByRegexp(originValue) != ""
}
func (r *radixMatcher) findByRegexp(value string) (regexpPattern string) {
for _, rx := range r.regexps {
if rx.MatchString(value) {
return rx.String()
}
}
return ""
}
func (r *radixMatcher) RegisterRegexp(regexpPattern string) {
pattern := regexp.MustCompile(regexpPattern)
r.regexps = append(r.regexps, pattern)
}
func (r *radixMatcher) Register(pattern string) {
if len(pattern) == 0 {
panic("unable to handle empty pattern")
}
if r.caseInsensitive {
pattern = strings.ToLower(pattern)
}
r.saved = append(r.saved, pattern)
node := r.root
lastIdx := len([]rune(pattern)) - 1
for i, v := range pattern {
nextNode, ok := node.next[v]
if !ok {
nextNode = &radixNode{
next: make(map[rune]*radixNode),
}
node.next[v] = nextNode
}
node = nextNode
if v == '*' {
return
}
if i != lastIdx {
continue
}
node.exact = true
}
}
func (r *radixMatcher) Patterns() []string {
out := make([]string, len(r.saved))
copy(out, r.saved)
return out
}

View File

@ -0,0 +1,118 @@
package matcher_test
import (
"testing"
"git.loyso.art/frx/eway/internal/matcher"
)
func TestRadixMatcherWithPattern(t *testing.T) {
m := matcher.NewRadix()
m.Register("aloha")
m.Register("hawaii")
m.Register("te*")
var tt = []struct {
name string
in string
pattern string
}{{
name: "should match exact",
in: "aloha",
pattern: "aloha",
}, {
name: "should match exact 2",
in: "hawaii",
pattern: "hawaii",
}, {
name: "should match pattern",
in: "test",
pattern: "te*",
}, {
name: "should not match",
in: "alohe",
}, {
name: "should not match 2",
in: "whoa",
}}
for _, tc := range tt {
tc := tc
t.Run(tc.name, func(t *testing.T) {
pattern := m.MatchByPattern(tc.in)
if pattern != tc.pattern {
t.Errorf("expected %s got %s", tc.pattern, pattern)
}
})
}
}
func TestRadixMatcher(t *testing.T) {
m := matcher.NewRadix()
m.Register("aloha")
m.Register("hawaii")
m.Register("te*")
var tt = []struct {
name string
in string
match bool
}{{
name: "should match exact",
in: "aloha",
match: true,
}, {
name: "should match exact 2",
in: "hawaii",
match: true,
}, {
name: "should match pattern",
in: "test",
match: true,
}, {
name: "should not match",
in: "alohe",
}, {
name: "should not match 2",
in: "whoa",
}}
for _, tc := range tt {
tc := tc
t.Run(tc.name, func(t *testing.T) {
match := m.Match(tc.in)
if match != tc.match {
t.Errorf("expected %t got %t", tc.match, match)
}
})
}
}
func TestRadixMatcherWildcard(t *testing.T) {
m := matcher.NewRadix()
m.Register("*")
var tt = []struct {
name string
in string
match bool
}{{
name: "should match exact",
in: "aloha",
match: true,
}, {
name: "should match exact 2",
in: "hawaii",
match: true,
}}
for _, tc := range tt {
tc := tc
t.Run(tc.name, func(t *testing.T) {
match := m.Match(tc.in)
if match != tc.match {
t.Errorf("expected %t got %t", tc.match, match)
}
})
}
}