From e5c1152ee156cfb88139ff2122dfaa939a76f3d5 Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Wed, 28 Aug 2024 17:54:05 +0200 Subject: [PATCH] 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.
Contributors' checklist... - [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).
--------- Signed-off-by: Jeff Thompson --- .../gno.land/p/demo/avlhelpers/avlhelpers.gno | 41 +++++++++ examples/gno.land/p/demo/avlhelpers/gno.mod | 3 + .../p/demo/avlhelpers/z_0_filetest.gno | 91 +++++++++++++++++++ examples/gno.land/r/demo/users/gno.mod | 1 + examples/gno.land/r/demo/users/users.gno | 7 ++ .../gno.land/r/demo/users/z_12_filetest.gno | 49 ++++++++++ 6 files changed, 192 insertions(+) create mode 100644 examples/gno.land/p/demo/avlhelpers/avlhelpers.gno create mode 100644 examples/gno.land/p/demo/avlhelpers/gno.mod create mode 100644 examples/gno.land/p/demo/avlhelpers/z_0_filetest.gno create mode 100644 examples/gno.land/r/demo/users/z_12_filetest.gno diff --git a/examples/gno.land/p/demo/avlhelpers/avlhelpers.gno b/examples/gno.land/p/demo/avlhelpers/avlhelpers.gno new file mode 100644 index 00000000000..27842932dd3 --- /dev/null +++ b/examples/gno.land/p/demo/avlhelpers/avlhelpers.gno @@ -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 +} diff --git a/examples/gno.land/p/demo/avlhelpers/gno.mod b/examples/gno.land/p/demo/avlhelpers/gno.mod new file mode 100644 index 00000000000..559f60975cf --- /dev/null +++ b/examples/gno.land/p/demo/avlhelpers/gno.mod @@ -0,0 +1,3 @@ +module gno.land/p/demo/avlhelpers + +require gno.land/p/demo/avl v0.0.0-latest diff --git a/examples/gno.land/p/demo/avlhelpers/z_0_filetest.gno b/examples/gno.land/p/demo/avlhelpers/z_0_filetest.gno new file mode 100644 index 00000000000..1c7873e297a --- /dev/null +++ b/examples/gno.land/p/demo/avlhelpers/z_0_filetest.gno @@ -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 diff --git a/examples/gno.land/r/demo/users/gno.mod b/examples/gno.land/r/demo/users/gno.mod index a2ee2ea86ba..61b11c09b80 100644 --- a/examples/gno.land/r/demo/users/gno.mod +++ b/examples/gno.land/r/demo/users/gno.mod @@ -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 ) diff --git a/examples/gno.land/r/demo/users/users.gno b/examples/gno.land/r/demo/users/users.gno index 9b8e93b579b..4a0b9c1caf7 100644 --- a/examples/gno.land/r/demo/users/users.gno +++ b/examples/gno.land/r/demo/users/users.gno @@ -7,6 +7,7 @@ import ( "strings" "gno.land/p/demo/avl" + "gno.land/p/demo/avlhelpers" "gno.land/p/demo/users" ) @@ -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 { diff --git a/examples/gno.land/r/demo/users/z_12_filetest.gno b/examples/gno.land/r/demo/users/z_12_filetest.gno new file mode 100644 index 00000000000..0fb7d27bd34 --- /dev/null +++ b/examples/gno.land/r/demo/users/z_12_filetest.gno @@ -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