Skip to content

Commit

Permalink
feat: enable GetLongerMatches API with utest
Browse files Browse the repository at this point in the history
  • Loading branch information
Jia He authored and ihexxa committed Jul 5, 2022
1 parent 3c7dd9a commit 43f4adc
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 4 deletions.
98 changes: 95 additions & 3 deletions radix.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ func (n *node) NextNode() (Node, bool) {

// Value returns node's value, it returns nil if there is no
func (n *node) Value() (interface{}, bool) {
return n.Leaf.Val, n.Leaf != nil
if n.Leaf != nil {
return n.Leaf.Val, true
}
return nil, false
}

// Extra returns node's Extra Info
Expand Down Expand Up @@ -73,7 +76,7 @@ type RTree struct {
}

// return common prefix's offset of s1 and s2, in byte
// s1[:offset] == s2[:offset]
// s1[:offset+1] == s2[:offset+1]
func commonPrefixOffset(s1, s2 string) int {
i := 0
runes1, runes2 := []rune(s1), []rune(s2)
Expand Down Expand Up @@ -454,7 +457,96 @@ func (T *RTree) GetAllPrefixMatches(key string) map[string]interface{} {
return resultMap
}

// GetBestMatch returns the longest match in the tree according to the key
type traverseLog struct {
n *node
base string
}

// GetLongerMatches returns at most `limmit` matches which are longer than the key
// if no match is found, it returns an empty map
func (T *RTree) GetLongerMatches(key string, limit int) map[string]interface{} {
T.m.RLock()
defer T.m.RUnlock()

resultMap := map[string]interface{}{}
if T.root == nil {
return resultMap
} else if len(key) == 0 {
return resultMap
}

var ok bool
var rune1 rune
var matchedNode *node
node1 := T.root
pathSuffix := key
baseOffset := 0 // key[:baseOffset] is matched
for {
if node1 == nil {
return resultMap
}

rune1 = getRune1(pathSuffix)
matchedNode, ok = node1.Idx[rune1]
if !ok {
return resultMap
}

offset := commonPrefixOffset(matchedNode.Prefix, pathSuffix)
if offset == -1 {
// this is impossible
panic(errImpossible(matchedNode.Prefix, key))
} else if offset == len(matchedNode.Prefix)-1 && offset < len(pathSuffix)-1 {
pathSuffix = pathSuffix[offset+1:]
node1 = matchedNode.Children
baseOffset += offset + 1
continue
} else if offset == len(pathSuffix)-1 {
break
}
return resultMap
}

if matchedNode.Leaf != nil {
resultMap[key[:baseOffset]+matchedNode.Prefix] = matchedNode.Leaf.Val
}
// start from next level becasue matchedNode's siblings are not results
// traverse from the matchedNode and return values
queue := []*traverseLog{&traverseLog{
n: matchedNode.Children,
base: key[:baseOffset] + matchedNode.Prefix,
}}
for len(queue) > 0 {
tlog := queue[0]
queue = queue[1:]
if tlog.n == nil {
continue
}

if tlog.n.Leaf != nil {
resultMap[tlog.base+tlog.n.Prefix] = tlog.n.Leaf.Val
if len(resultMap) > limit {
break
}
}
if tlog.n.Next != nil {
queue = append(queue, &traverseLog{
n: tlog.n.Next,
base: tlog.base,
})
}
if tlog.n.Children != nil {
queue = append(queue, &traverseLog{
n: tlog.n.Children,
base: tlog.base + tlog.n.Prefix,
})
}
}

return resultMap
}

// GetBestMatch returns the longest match from all existings values which key is short than the input key
// if there is no match, it returns empty string, nil and false
func (T *RTree) GetBestMatch(key string) (string, interface{}, bool) {
T.m.RLock()
Expand Down
46 changes: 45 additions & 1 deletion radix_random_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"flag"
"fmt"
"math/rand"
"strings"
"testing"
"time"
)
Expand All @@ -14,7 +15,7 @@ const (
)

var (
// use -args to input these options: "go test -args -d=true"
// use -args to input these options: "go test -args -h=10"
actionCount = flag.Int("c", 30, "how many actions will be applied on RTree and map")
insertRatio = flag.Int("i", 60, "control the the ratio between insert action and remove action")
maxLen = flag.Int("l", 5, "how long will random string be generated")
Expand All @@ -29,6 +30,7 @@ func TestWithRandomKeys(t *testing.T) {
seedRand()
for i := 0; i < *testRound; i++ {
randomTest(t)
longerKeyTest(t)
}
}

Expand Down Expand Up @@ -118,6 +120,31 @@ func randomTest(t *testing.T) {
}
}

func longerKeyTest(t *testing.T) {
var actions []string
tree := NewRTree()
dict := make(map[string]string)
randomStrings := GetTestStrings()

for i := 0; i < *actionCount; i++ {
key := randomStrings[rand.Intn(len(randomStrings))]
doRandomAction(&actions, key, tree, dict)
}

randomKey := randomStrings[rand.Intn(len(randomStrings))]
end := rand.Intn(len(randomKey)) + 1
start := rand.Intn(end)
randomKey = randomKey[start:end]
longerMatches := tree.GetLongerMatches(randomKey, 5)
if !checkLongerMatches(randomKey, longerMatches) {
fmt.Printf("longerMatches of (%s): %+v\n", randomKey, longerMatches)
printActions(actions)
printRTree(tree)
printMap(dict)
t.Fatalf("incorrect longer matches")
}
}

func checkPrefixMatches(
key string,
prefixes map[string]interface{},
Expand All @@ -138,6 +165,23 @@ func checkPrefixMatches(
return true
}

// checkLongerMatches only checks if key is the prefix of each longerMatches
// Becasue currently, besides the first one,
// it doesn't return results by the order of how much it is closed to the key
func checkLongerMatches(
key string,
longerMatches map[string]interface{},
) bool {
for longerMatch := range longerMatches {
if !strings.HasPrefix(longerMatch, key) {
fmt.Printf("%s is not a prefix of %s \n", key, longerMatch)
return false
}
}

return true
}

func GetTestStrings() []string {
var str []rune
var randomStrings []string
Expand Down

0 comments on commit 43f4adc

Please sign in to comment.