Skip to content

Commit e5c1152

Browse files
authored
feat: In r/demo/users, add ListUsersByPrefix (#1708)
GnoSocial uses r/demo/users to register users. A typical social app can search for other users by name, which requires a search box with partial match. The private variable `name2User` is an avl.Tree where the key is the user name, and avl.Tree `Iterate` can already iterate by start and end values. This PR has 2 commits: 1. Add a new package p/demo/avlhelpers with the function `ListKeysByPrefix` . Also add tests. 2. Use this in r/demo/users to add `ListUsersByPrefix` to return a list of user names starting from the given prefix. For example, `ListUsersByPrefix("g", 2)` returns a list of 2 names with the prefix "g": ``` ["george", "gnofan"] ``` In the GnoSocial demo app, we plan to use this in a search box. <details><summary>Contributors' checklist...</summary> - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md). </details> --------- Signed-off-by: Jeff Thompson <[email protected]>
1 parent 651f5aa commit e5c1152

File tree

6 files changed

+192
-0
lines changed

6 files changed

+192
-0
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package avlhelpers
2+
3+
import (
4+
"gno.land/p/demo/avl"
5+
)
6+
7+
// Iterate the keys in-order starting from the given prefix.
8+
// It calls the provided callback function for each key-value pair encountered.
9+
// If the callback returns true, the iteration is stopped.
10+
// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes.
11+
func IterateByteStringKeysByPrefix(tree avl.Tree, prefix string, cb avl.IterCbFn) {
12+
end := ""
13+
n := len(prefix)
14+
// To make the end of the search, increment the final character ASCII by one.
15+
for n > 0 {
16+
if ascii := int(prefix[n-1]); ascii < 0xff {
17+
end = prefix[0:n-1] + string(ascii+1)
18+
break
19+
}
20+
21+
// The last character is 0xff. Try the previous character.
22+
n--
23+
}
24+
25+
tree.Iterate(prefix, end, cb)
26+
}
27+
28+
// Get a list of keys starting from the given prefix. Limit the
29+
// number of results to maxResults.
30+
// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes.
31+
func ListByteStringKeysByPrefix(tree avl.Tree, prefix string, maxResults int) []string {
32+
result := []string{}
33+
IterateByteStringKeysByPrefix(tree, prefix, func(key string, value interface{}) bool {
34+
result = append(result, key)
35+
if len(result) >= maxResults {
36+
return true
37+
}
38+
return false
39+
})
40+
return result
41+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module gno.land/p/demo/avlhelpers
2+
3+
require gno.land/p/demo/avl v0.0.0-latest
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// PKGPATH: gno.land/r/test
2+
package test
3+
4+
import (
5+
"encoding/hex"
6+
7+
"gno.land/p/demo/avl"
8+
"gno.land/p/demo/avlhelpers"
9+
"gno.land/p/demo/ufmt"
10+
)
11+
12+
func main() {
13+
tree := avl.Tree{}
14+
15+
{
16+
// Empty tree.
17+
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "", 10)
18+
println(ufmt.Sprintf("# matches: %d", len(matches)))
19+
}
20+
21+
tree.Set("alice", "")
22+
tree.Set("andy", "")
23+
tree.Set("bob", "")
24+
25+
{
26+
// Match only alice.
27+
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "al", 10)
28+
println(ufmt.Sprintf("# matches: %d", len(matches)))
29+
println("match: " + matches[0])
30+
}
31+
32+
{
33+
// Match alice and andy.
34+
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "a", 10)
35+
println(ufmt.Sprintf("# matches: %d", len(matches)))
36+
println("match: " + matches[0])
37+
println("match: " + matches[1])
38+
}
39+
40+
{
41+
// Match alice and andy limited to 1.
42+
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "a", 1)
43+
println(ufmt.Sprintf("# matches: %d", len(matches)))
44+
println("match: " + matches[0])
45+
}
46+
47+
tree = avl.Tree{}
48+
tree.Set("a\xff", "")
49+
tree.Set("a\xff\xff", "")
50+
tree.Set("b", "")
51+
tree.Set("\xff\xff\x00", "")
52+
53+
{
54+
// Match only "a\xff\xff".
55+
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "a\xff\xff", 10)
56+
println(ufmt.Sprintf("# matches: %d", len(matches)))
57+
println(ufmt.Sprintf("match: %s", hex.EncodeToString([]byte(matches[0]))))
58+
}
59+
60+
{
61+
// Match "a\xff" and "a\xff\xff".
62+
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "a\xff", 10)
63+
println(ufmt.Sprintf("# matches: %d", len(matches)))
64+
println(ufmt.Sprintf("match: %s", hex.EncodeToString([]byte(matches[0]))))
65+
println(ufmt.Sprintf("match: %s", hex.EncodeToString([]byte(matches[1]))))
66+
}
67+
68+
{
69+
// Edge case: Match only "\xff\xff\x00".
70+
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "\xff\xff", 10)
71+
println(ufmt.Sprintf("# matches: %d", len(matches)))
72+
println(ufmt.Sprintf("match: %s", hex.EncodeToString([]byte(matches[0]))))
73+
}
74+
}
75+
76+
// Output:
77+
// # matches: 0
78+
// # matches: 1
79+
// match: alice
80+
// # matches: 2
81+
// match: alice
82+
// match: andy
83+
// # matches: 1
84+
// match: alice
85+
// # matches: 1
86+
// match: 61ffff
87+
// # matches: 2
88+
// match: 61ff
89+
// match: 61ffff
90+
// # matches: 1
91+
// match: ffff00

examples/gno.land/r/demo/users/gno.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ module gno.land/r/demo/users
22

33
require (
44
gno.land/p/demo/avl v0.0.0-latest
5+
gno.land/p/demo/avlhelpers v0.0.0-latest
56
gno.land/p/demo/users v0.0.0-latest
67
)

examples/gno.land/r/demo/users/users.gno

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
"gno.land/p/demo/avl"
10+
"gno.land/p/demo/avlhelpers"
1011
"gno.land/p/demo/users"
1112
)
1213

@@ -255,6 +256,12 @@ func GetUserByAddressOrName(input users.AddressOrName) *users.User {
255256
return GetUserByAddress(std.Address(input))
256257
}
257258

259+
// Get a list of user names starting from the given prefix. Limit the
260+
// number of results to maxResults. (This can be used for a name search tool.)
261+
func ListUsersByPrefix(prefix string, maxResults int) []string {
262+
return avlhelpers.ListByteStringKeysByPrefix(name2User, prefix, maxResults)
263+
}
264+
258265
func Resolve(input users.AddressOrName) std.Address {
259266
name, isName := input.GetName()
260267
if !isName {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package main
2+
3+
// SEND: 200000000ugnot
4+
5+
import (
6+
"strconv"
7+
8+
"gno.land/r/demo/users"
9+
)
10+
11+
func main() {
12+
users.Register("", "alicia", "my profile")
13+
14+
{
15+
// Normal usage
16+
names := users.ListUsersByPrefix("a", 1)
17+
println("# names: " + strconv.Itoa(len(names)))
18+
println("name: " + names[0])
19+
}
20+
21+
{
22+
// Empty prefix: match all
23+
names := users.ListUsersByPrefix("", 1)
24+
println("# names: " + strconv.Itoa(len(names)))
25+
println("name: " + names[0])
26+
}
27+
28+
{
29+
// The prefix is before "alicia"
30+
names := users.ListUsersByPrefix("alich", 1)
31+
println("# names: " + strconv.Itoa(len(names)))
32+
}
33+
34+
{
35+
// The prefix is after the last name
36+
names := users.ListUsersByPrefix("y", 10)
37+
println("# names: " + strconv.Itoa(len(names)))
38+
}
39+
40+
// More tests are in p/demo/avlhelpers
41+
}
42+
43+
// Output:
44+
// # names: 1
45+
// name: alicia
46+
// # names: 1
47+
// name: alicia
48+
// # names: 0
49+
// # names: 0

0 commit comments

Comments
 (0)