Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(examples): add p/moul/addrset #3448

Merged
merged 4 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions examples/gno.land/p/moul/addrset/addrset.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Package addrset provides a specialized set data structure for managing unique Gno addresses.
//
// It is built on top of an AVL tree for efficient operations and maintains addresses in sorted order.
// This package is particularly useful when you need to:
// - Track a collection of unique addresses (e.g., for whitelists, participants, etc.)
// - Efficiently check address membership
// - Support pagination when displaying addresses
//
// Example usage:
//
// import (
// "std"
// "gno.land/p/moul/addrset"
// )
//
// func MyHandler() {
// // Create a new address set
// var set addrset.Set
//
// // Add some addresses
// addr1 := std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5")
// addr2 := std.Address("g1sss5g0rkqr88k4u648yd5d3l9t4d8vvqwszqth")
//
// set.Add(addr1) // returns true (newly added)
// set.Add(addr2) // returns true (newly added)
// set.Add(addr1) // returns false (already exists)
//
// // Check membership
// if set.Has(addr1) {
// // addr1 is in the set
// }
//
// // Get size
// size := set.Size() // returns 2
//
// // Iterate with pagination (10 items per page, starting at offset 0)
// set.IterateByOffset(0, 10, func(addr std.Address) bool {
// // Process addr
// return false // continue iteration
// })
//
// // Remove an address
// set.Remove(addr1) // returns true (was present)
// set.Remove(addr1) // returns false (not present)
// }
package addrset

import (
"std"

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

type Set struct {
tree avl.Tree
}

// Add inserts an address into the set.
// Returns true if the address was newly added, false if it already existed.
func (s *Set) Add(addr std.Address) bool {
return !s.tree.Set(string(addr), nil)
}

// Remove deletes an address from the set.
// Returns true if the address was found and removed, false if it didn't exist.
func (s *Set) Remove(addr std.Address) bool {
_, removed := s.tree.Remove(string(addr))
return removed
}

// Has checks if an address exists in the set.
func (s *Set) Has(addr std.Address) bool {
return s.tree.Has(string(addr))
}

// Size returns the number of addresses in the set.
func (s *Set) Size() int {
return s.tree.Size()
}

// IterateByOffset walks through addresses starting at the given offset.
// The callback should return true to stop iteration.
func (s *Set) IterateByOffset(offset int, count int, cb func(addr std.Address) bool) {
s.tree.IterateByOffset(offset, count, func(key string, _ interface{}) bool {
return cb(std.Address(key))
})
}

// ReverseIterateByOffset walks through addresses in reverse order starting at the given offset.
// The callback should return true to stop iteration.
func (s *Set) ReverseIterateByOffset(offset int, count int, cb func(addr std.Address) bool) {
s.tree.ReverseIterateByOffset(offset, count, func(key string, _ interface{}) bool {
return cb(std.Address(key))
})
}

// Tree returns the underlying AVL tree for advanced usage.
func (s *Set) Tree() avl.ITree {
return &s.tree
}
174 changes: 174 additions & 0 deletions examples/gno.land/p/moul/addrset/addrset_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package addrset

import (
"std"
"testing"

"gno.land/p/demo/uassert"
)

func TestSet(t *testing.T) {
addr1 := std.Address("addr1")
addr2 := std.Address("addr2")
addr3 := std.Address("addr3")

tests := []struct {
name string
actions func(s *Set)
size int
has map[std.Address]bool
addrs []std.Address // for iteration checks
}{
{
name: "empty set",
actions: func(s *Set) {},
size: 0,
has: map[std.Address]bool{addr1: false},
},
{
name: "single address",
actions: func(s *Set) {
s.Add(addr1)
},
size: 1,
has: map[std.Address]bool{
addr1: true,
addr2: false,
},
addrs: []std.Address{addr1},
},
{
name: "multiple addresses",
actions: func(s *Set) {
s.Add(addr1)
s.Add(addr2)
s.Add(addr3)
},
size: 3,
has: map[std.Address]bool{
addr1: true,
addr2: true,
addr3: true,
},
addrs: []std.Address{addr1, addr2, addr3},
},
{
name: "remove address",
actions: func(s *Set) {
s.Add(addr1)
s.Add(addr2)
s.Remove(addr1)
},
size: 1,
has: map[std.Address]bool{
addr1: false,
addr2: true,
},
addrs: []std.Address{addr2},
},
{
name: "duplicate adds",
actions: func(s *Set) {
uassert.True(t, s.Add(addr1)) // first add returns true
uassert.False(t, s.Add(addr1)) // second add returns false
uassert.True(t, s.Remove(addr1)) // remove existing returns true
uassert.False(t, s.Remove(addr1)) // remove non-existing returns false
},
size: 0,
has: map[std.Address]bool{
addr1: false,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var set Set

// Execute test actions
tt.actions(&set)

// Check size
uassert.Equal(t, tt.size, set.Size())

// Check existence
for addr, expected := range tt.has {
uassert.Equal(t, expected, set.Has(addr))
}

// Check iteration if addresses are specified
if tt.addrs != nil {
collected := []std.Address{}
set.IterateByOffset(0, 10, func(addr std.Address) bool {
collected = append(collected, addr)
return false
})

// Check length
uassert.Equal(t, len(tt.addrs), len(collected))

// Check each address
for i, addr := range tt.addrs {
uassert.Equal(t, addr, collected[i])
}
}
})
}
}

func TestSetIterationLimits(t *testing.T) {
tests := []struct {
name string
addrs []std.Address
offset int
limit int
expected int
}{
{
name: "zero offset full list",
addrs: []std.Address{"a1", "a2", "a3"},
offset: 0,
limit: 10,
expected: 3,
},
{
name: "offset with limit",
addrs: []std.Address{"a1", "a2", "a3", "a4"},
offset: 1,
limit: 2,
expected: 2,
},
{
name: "offset beyond size",
addrs: []std.Address{"a1", "a2"},
offset: 3,
limit: 1,
expected: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var set Set
for _, addr := range tt.addrs {
set.Add(addr)
}

// Test forward iteration
count := 0
set.IterateByOffset(tt.offset, tt.limit, func(addr std.Address) bool {
count++
return false
})
uassert.Equal(t, tt.expected, count)

// Test reverse iteration
count = 0
set.ReverseIterateByOffset(tt.offset, tt.limit, func(addr std.Address) bool {
count++
return false
})
uassert.Equal(t, tt.expected, count)
})
}
}
1 change: 1 addition & 0 deletions examples/gno.land/p/moul/addrset/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/moul/addrset
Loading