Skip to content

Commit

Permalink
feat: In r/demo/users, add ListUsersByPrefix (#1708)
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
jefft0 authored Aug 28, 2024
1 parent 651f5aa commit e5c1152
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 0 deletions.
41 changes: 41 additions & 0 deletions examples/gno.land/p/demo/avlhelpers/avlhelpers.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package avlhelpers

import (
"gno.land/p/demo/avl"
)

// Iterate the keys in-order starting from the given prefix.
// It calls the provided callback function for each key-value pair encountered.
// If the callback returns true, the iteration is stopped.
// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes.
func IterateByteStringKeysByPrefix(tree avl.Tree, prefix string, cb avl.IterCbFn) {
end := ""
n := len(prefix)
// To make the end of the search, increment the final character ASCII by one.
for n > 0 {
if ascii := int(prefix[n-1]); ascii < 0xff {
end = prefix[0:n-1] + string(ascii+1)
break
}

// The last character is 0xff. Try the previous character.
n--
}

tree.Iterate(prefix, end, cb)
}

// Get a list of keys starting from the given prefix. Limit the
// number of results to maxResults.
// The prefix and keys are treated as byte strings, ignoring possible multi-byte Unicode runes.
func ListByteStringKeysByPrefix(tree avl.Tree, prefix string, maxResults int) []string {
result := []string{}
IterateByteStringKeysByPrefix(tree, prefix, func(key string, value interface{}) bool {
result = append(result, key)
if len(result) >= maxResults {
return true
}
return false
})
return result
}
3 changes: 3 additions & 0 deletions examples/gno.land/p/demo/avlhelpers/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module gno.land/p/demo/avlhelpers

require gno.land/p/demo/avl v0.0.0-latest
91 changes: 91 additions & 0 deletions examples/gno.land/p/demo/avlhelpers/z_0_filetest.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// PKGPATH: gno.land/r/test
package test

import (
"encoding/hex"

"gno.land/p/demo/avl"
"gno.land/p/demo/avlhelpers"
"gno.land/p/demo/ufmt"
)

func main() {
tree := avl.Tree{}

{
// Empty tree.
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "", 10)
println(ufmt.Sprintf("# matches: %d", len(matches)))
}

tree.Set("alice", "")
tree.Set("andy", "")
tree.Set("bob", "")

{
// Match only alice.
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "al", 10)
println(ufmt.Sprintf("# matches: %d", len(matches)))
println("match: " + matches[0])
}

{
// Match alice and andy.
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "a", 10)
println(ufmt.Sprintf("# matches: %d", len(matches)))
println("match: " + matches[0])
println("match: " + matches[1])
}

{
// Match alice and andy limited to 1.
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "a", 1)
println(ufmt.Sprintf("# matches: %d", len(matches)))
println("match: " + matches[0])
}

tree = avl.Tree{}
tree.Set("a\xff", "")
tree.Set("a\xff\xff", "")
tree.Set("b", "")
tree.Set("\xff\xff\x00", "")

{
// Match only "a\xff\xff".
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "a\xff\xff", 10)
println(ufmt.Sprintf("# matches: %d", len(matches)))
println(ufmt.Sprintf("match: %s", hex.EncodeToString([]byte(matches[0]))))
}

{
// Match "a\xff" and "a\xff\xff".
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "a\xff", 10)
println(ufmt.Sprintf("# matches: %d", len(matches)))
println(ufmt.Sprintf("match: %s", hex.EncodeToString([]byte(matches[0]))))
println(ufmt.Sprintf("match: %s", hex.EncodeToString([]byte(matches[1]))))
}

{
// Edge case: Match only "\xff\xff\x00".
matches := avlhelpers.ListByteStringKeysByPrefix(tree, "\xff\xff", 10)
println(ufmt.Sprintf("# matches: %d", len(matches)))
println(ufmt.Sprintf("match: %s", hex.EncodeToString([]byte(matches[0]))))
}
}

// Output:
// # matches: 0
// # matches: 1
// match: alice
// # matches: 2
// match: alice
// match: andy
// # matches: 1
// match: alice
// # matches: 1
// match: 61ffff
// # matches: 2
// match: 61ff
// match: 61ffff
// # matches: 1
// match: ffff00
1 change: 1 addition & 0 deletions examples/gno.land/r/demo/users/gno.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ module gno.land/r/demo/users

require (
gno.land/p/demo/avl v0.0.0-latest
gno.land/p/demo/avlhelpers v0.0.0-latest
gno.land/p/demo/users v0.0.0-latest
)
7 changes: 7 additions & 0 deletions examples/gno.land/r/demo/users/users.gno
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"gno.land/p/demo/avl"
"gno.land/p/demo/avlhelpers"
"gno.land/p/demo/users"
)

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

// Get a list of user names starting from the given prefix. Limit the
// number of results to maxResults. (This can be used for a name search tool.)
func ListUsersByPrefix(prefix string, maxResults int) []string {
return avlhelpers.ListByteStringKeysByPrefix(name2User, prefix, maxResults)
}

func Resolve(input users.AddressOrName) std.Address {
name, isName := input.GetName()
if !isName {
Expand Down
49 changes: 49 additions & 0 deletions examples/gno.land/r/demo/users/z_12_filetest.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package main

// SEND: 200000000ugnot

import (
"strconv"

"gno.land/r/demo/users"
)

func main() {
users.Register("", "alicia", "my profile")

{
// Normal usage
names := users.ListUsersByPrefix("a", 1)
println("# names: " + strconv.Itoa(len(names)))
println("name: " + names[0])
}

{
// Empty prefix: match all
names := users.ListUsersByPrefix("", 1)
println("# names: " + strconv.Itoa(len(names)))
println("name: " + names[0])
}

{
// The prefix is before "alicia"
names := users.ListUsersByPrefix("alich", 1)
println("# names: " + strconv.Itoa(len(names)))
}

{
// The prefix is after the last name
names := users.ListUsersByPrefix("y", 10)
println("# names: " + strconv.Itoa(len(names)))
}

// More tests are in p/demo/avlhelpers
}

// Output:
// # names: 1
// name: alicia
// # names: 1
// name: alicia
// # names: 0
// # names: 0

0 comments on commit e5c1152

Please sign in to comment.