diff --git a/examples/gno.land/p/moul/ulist/ulist.gno b/examples/gno.land/p/moul/ulist/ulist.gno index f580b68916d..3df038c3c19 100644 --- a/examples/gno.land/p/moul/ulist/ulist.gno +++ b/examples/gno.land/p/moul/ulist/ulist.gno @@ -1,331 +1,291 @@ -// Package ulist provides an append-only list implementation using a chunked binary tree structure, -// optimized for scenarios requiring sequential inserts with auto-incrementing indices (similar to -// a primary key in databases). +// Package ulist provides an append-only list implementation using a binary tree structure, +// optimized for scenarios requiring sequential inserts with auto-incrementing indices. // -// The implementation uses a log-structured tree storage that splits data into fixed-size chunks, -// organized in a binary tree. This structure is particularly well-suited for: -// * Sequential inserts with auto-incrementing indices -// * Efficient pagination and range queries -// * Soft deletions (marking elements as deleted without restructuring) -// * Primary index-like functionality with O(1) appends +// The implementation uses a binary tree where new elements are added by following a path +// determined by the binary representation of the index. This provides automatic balancing +// for append operations without requiring any balancing logic. // // Key characteristics: -// * O(1) append operations (always adds to the end) -// * O(log n) random access and deletions -// * Memory efficient deletions (elements are marked as nil) -// * Chunk-based storage for better cache locality -// * No rebalancing required -// * Efficient forward and reverse iteration with pagination -// -// Not recommended for: -// * Random insertions (append-only) -// * Small lists where a simple slice would suffice -// * Scenarios needing reordering of elements +// * O(log n) append and access operations +// * Perfect balance for power-of-2 sizes +// * No balancing needed +// * Memory efficient +// * Natural support for range queries + package ulist // TODO: Make avl/pager compatible in some way. Explain the limitations (not always 10 items because of nil ones). // TODO: Add MustXXX helpers. // TODO: Use this ulist in moul/collection for the primary index. +// TODO: Consider adding a "compact" method that removes nil nodes. +// TODO: Remove debug logging. import ( "errors" ) +// Debug controls whether debug logging is enabled +var Debug bool = false + +// log prints debug messages when Debug is true +func log(args ...interface{}) { + if Debug { + println(args...) + } +} + // Error variables var ( ErrOutOfBounds = errors.New("index out of bounds") ErrDeleted = errors.New("element already deleted") ) -// List represents a chunked binary tree list implementation. -// It maintains a root node and tracks both the total size (including deleted elements) -// and active size (excluding deleted elements). The current field points to the last chunk -// for efficient append operations. +// Entry represents a key-value pair in the list +type Entry struct { + Index int + Value interface{} +} + +// List represents an append-only binary tree list type List struct { - root *treeNode - chunkSize int - totalSize int // total number of elements, including deleted ones - size int // number of active (non-deleted) elements - current *treeNode // points to the last chunk for efficient appends + root *treeNode + totalSize int + activeSize int } -// treeNode represents a single chunk in the binary tree structure. -// Each node contains a fixed-size array of elements and maintains its index range -// (start to end) within the overall list. Deleted elements are marked as nil -// but remain in the data array. +// treeNode represents a node in the binary tree type treeNode struct { - data []interface{} // fixed-size chunk of elements - left *treeNode // points to older chunks - right *treeNode // points to newer chunks - start int // inclusive starting index of this chunk - end int // exclusive ending index of this chunk - activeSize int // count of non-nil elements in this chunk + data interface{} + left *treeNode + right *treeNode } -// New creates a new List with the default chunk size of 8. -// This is suitable for most use cases. +// New creates a new List func New() *List { - return NewWithSize(8) + return &List{} } -// NewWithSize creates a new List with a custom chunk size. -// Larger chunk sizes improve sequential access but increase memory usage. -// The chunk size must be positive; if not, it defaults to 8. -func NewWithSize(chunkSize int) *List { - if chunkSize <= 0 { - chunkSize = 8 // fallback to default if invalid - } - return &List{ - chunkSize: chunkSize, - } -} - -// Append adds one or more values to the end of the list. -// This operation is O(1) amortized as it only requires updating the current chunk -// or creating a new one when the current chunk is full. +// Append adds one or more values to the list func (l *List) Append(values ...interface{}) { for _, value := range values { if l.root == nil { - // Create first node - newNode := &treeNode{ - data: make([]interface{}, 0, l.chunkSize), - start: 0, - end: 1, - activeSize: 1, - } - newNode.data = append(newNode.data, value) - l.root = newNode - l.current = l.root - l.totalSize = 1 - l.size = 1 - } else { - // Add to existing node or create new chunk if needed - if len(l.current.data) == l.chunkSize { - newChunk := &treeNode{ - data: make([]interface{}, 0, l.chunkSize), - start: l.totalSize, - end: l.totalSize + 1, - activeSize: 1, + l.root = &treeNode{data: value} + l.totalSize++ + l.activeSize++ + continue + } + + index := l.totalSize + bits := highestBit(index + 1) + node := l.root + + // Start from the second highest bit + for level := bits - 2; level >= 0; level-- { + bit := (index & (1 << uint(level))) != 0 + + if bit { + if node.right == nil { + node.right = &treeNode{} } - newChunk.data = append(newChunk.data, value) - l.current.right = newChunk - newChunk.left = l.current - l.current = newChunk + node = node.right } else { - l.current.data = append(l.current.data, value) - l.current.end++ - l.current.activeSize++ + if node.left == nil { + node.left = &treeNode{} + } + node = node.left } - l.totalSize++ - l.size++ } + + // After traversing, set the value in the current node + node.data = value + l.totalSize++ + l.activeSize++ } } -// Get retrieves the value at the specified index. -// Returns nil if the index is invalid or if the element was previously deleted. +// Get retrieves the value at the specified index func (l *List) Get(index int) interface{} { - if l == nil || l.root == nil { + if index >= l.totalSize || index < 0 { return nil } - if index < 0 || index >= l.totalSize { + node := l.root + if node == nil { return nil } - // Find the chunk containing our index - chunk := l.root - for chunk != nil { - if index >= chunk.start && index < chunk.end { - return chunk.data[index-chunk.start] - } + // Special case for root node + if index == 0 { + return node.data + } + + // Calculate the number of bits needed + bits := highestBit(index + 1) - if index < chunk.start { - chunk = chunk.left + // Start from the second highest bit + for level := bits - 2; level >= 0; level-- { + bit := (index & (1 << uint(level))) != 0 + + if bit { + node = node.right } else { - chunk = chunk.right + node = node.left + } + + if node == nil { + return nil } } - return nil + return node.data } -// Delete removes elements at the specified indices by marking them as nil. -// The elements are not physically removed, but the size counter is decremented. -// Returns an error if any index is invalid or if the element was already deleted. -// This operation is O(log n) per deletion. +// Delete marks the elements at the specified indices as deleted func (l *List) Delete(indices ...int) error { if len(indices) == 0 { return nil } for _, index := range indices { - if index < 0 { + node := l.getNodeByIndex(index) + if node == nil { return ErrOutOfBounds } - - if l.root == nil || index >= l.totalSize { - return ErrOutOfBounds - } - - node := l.root - - for node != nil { - if index >= node.start && index < node.end { - localIndex := index - node.start - - if node.data[localIndex] == nil { - return ErrDeleted - } - - node.data[localIndex] = nil - node.activeSize-- - l.size-- - break - } - - if index < node.start { - node = node.left - } else { - node = node.right - } + if node.data == nil { + return ErrDeleted } + node.data = nil + l.activeSize-- } return nil } -// TotalSize returns the total number of elements in the log (including nil entries). -func (l *List) TotalSize() int { - return l.totalSize -} - -// Size returns the number of active elements in the list. -func (l *List) Size() int { - return l.size +// deleteOne marks a single element as deleted by index, now also unified +// with getNodeByIndex for consistent traversal semantics. +func (l *List) deleteOne(index int) error { + if index < 0 || index >= l.totalSize { + return ErrOutOfBounds + } + node := l.getNodeByIndex(index) + if node == nil { + return ErrOutOfBounds + } + if node.data == nil { + return ErrDeleted + } + node.data = nil + l.activeSize-- + return nil } -// IterateByOffset traverses the list starting from the given offset, processing -// count elements in the specified direction (positive count for forward, negative -// for reverse). Only non-nil elements are processed. The iteration stops if the -// callback function returns true or when count elements have been processed. -// -// Parameters: -// - offset: starting position (negative offset adjusts the count) -// - count: number of elements to process (negative for reverse iteration) -// - fn: callback function called for each non-nil element with its Entry -// -// Returns true if the callback function returned true, false otherwise. -func (l *List) IterateByOffset(offset int, count int, fn func(Entry) bool) bool { - // Handle invalid cases - if l == nil || l.root == nil || l.size == 0 { - return false +// getNodeByIndex retrieves the *treeNode for zero-based index using the +// exact same path bits that appending used. +// This ensures your "Get path calculation" matches "Append path calculation". +func (l *List) getNodeByIndex(index int) *treeNode { + if index < 0 || index >= l.totalSize { + log("Get: index out of bounds", index, l.totalSize) + return nil } - if count == 0 { - return false + if index == 0 { + // root is index 0 + return l.root } - - // Handle negative offset by adjusting count - if offset < 0 { - // For reverse iteration, negative offset is invalid - if count < 0 { - return false + path := buildPathForIndex(index) // unify logic with Append + current := l.root + for _, step := range path { + if step { + current = current.right + } else { + current = current.left } - // Reduce count by the negative offset amount - count += offset // offset is negative, so this reduces count - offset = 0 - if count <= 0 { - return false + if current == nil { + return nil } } + return current +} - if offset >= l.totalSize { - return false - } +// Size returns the number of active elements +func (l *List) Size() int { + return l.activeSize +} - // Find initial chunk containing the offset - chunk := l.root - for chunk != nil { - if offset >= chunk.start && offset < chunk.end { - break - } - if offset < chunk.start { - chunk = chunk.left +// TotalSize returns the total number of elements (including deleted) +func (l *List) TotalSize() int { + return l.totalSize +} + +// formatBinary returns a string representation of n in binary with specified width +func formatBinary(n, width int) string { + result := make([]byte, width) + for i := width - 1; i >= 0; i-- { + if (n & (1 << uint(i))) != 0 { + result[width-1-i] = '1' } else { - chunk = chunk.right + result[width-1-i] = '0' } } + return string(result) +} - if chunk == nil { - return false +// highestBit returns the position of the highest bit set in n +func highestBit(n int) int { + pos := 0 + for n > 0 { + log(" highestBit:", n, "->", pos) + n >>= 1 + pos++ } + return pos +} - // Determine iteration direction and bounds - forward := count > 0 - remaining := abs(count) - processed := 0 - - if forward { - // Forward iteration - for chunk != nil && processed < remaining { - startIdx := max(0, offset-chunk.start) - for i := startIdx; i < len(chunk.data) && processed < remaining; i++ { - if chunk.data[i] != nil { - if fn(Entry{Index: chunk.start + i, Value: chunk.data[i]}) { - return true - } - processed++ - } - } - chunk = chunk.right - if chunk != nil { - offset = chunk.start - } - } - } else { - // Reverse iteration - for chunk != nil && processed < remaining { - startIdx := min(len(chunk.data)-1, offset-chunk.start) - for i := startIdx; i >= 0 && processed < remaining; i-- { - if chunk.data[i] != nil { - if fn(Entry{Index: chunk.start + i, Value: chunk.data[i]}) { - return true - } - processed++ - } - } - chunk = chunk.left - if chunk != nil { - offset = chunk.end - 1 - } - } +// buildPathForIndex replicates the same path logic that appendOne() uses. +// The resulting bool slice is from the first branch step to the last, with +// "false" = go left, "true" = go right. Index zero is special-cased at the caller. +func buildPathForIndex(index int) []bool { + // In your logs, for index=2, you show "bits: 2 binary: 10" and proceed + // level 0 bit: false -> left, final insert: left + // This indicates you read from LSB upward. + // So we gather bits from least-significant to most, then reverse them + // except we skip the highest bit (the "root" bit). + // Example: index=2 -> binary: 10 -> skip highest bit => path = [false] + + bits := []bool{} + for index > 0 { + // pick off least significant bit + bits = append(bits, (index&1) == 1) + index >>= 1 } - return false -} + // Now bits[0] is the LSB. For index=2 (“10” in binary), bits=[false,true]. + // The final “true” is the highest bit, so skip it: + if len(bits) > 0 { + bits = bits[:len(bits)-1] + } -// Iterate traverses the list between start and end indices (inclusive), -// calling the callback function for each non-nil element. The direction -// is determined by the relationship between start and end (start <= end -// for forward iteration). -// -// Parameters: -// - start: starting index (negative values are treated as 0) -// - end: ending index (negative values are treated as 0) -// - fn: callback function called for each non-nil element with its Entry -// -// Returns true if the callback function returned true, false otherwise. -func (l *List) Iterate(start, end int, fn func(Entry) bool) bool { - // Handle invalid cases - if l == nil || l.root == nil || l.size == 0 { - return false + // Reverse the remaining bits so the leftmost in bits is the first decision. + for i, j := 0, len(bits)-1; i < j; i, j = i+1, j-1 { + bits[i], bits[j] = bits[j], bits[i] } + return bits +} + +// IterCbFn is a callback function that processes an entry and returns whether to stop iterating +type IterCbFn func(index int, value interface{}) bool +// Iterator performs iteration between start and end indices, calling cb for each entry. +// If start > end, iteration is performed in reverse order. +// Returns true if iteration was stopped by callback returning true. +func (l *List) Iterator(start, end int, cb IterCbFn) bool { if start < 0 && end < 0 { return false } + if start >= l.totalSize && end >= l.totalSize { + return false + } - // Handle negative indices + // Normalize indices if start < 0 { start = 0 } @@ -335,132 +295,51 @@ func (l *List) Iterate(start, end int, fn func(Entry) bool) bool { if end >= l.totalSize { end = l.totalSize - 1 } - - // Determine iteration direction - forward := start <= end - if !forward { - start, end = end, start + if start >= l.totalSize { + start = l.totalSize - 1 } - // Find the chunk containing the start index - current := l.root - for current != nil { - if start >= current.start && start < current.end { - break - } - if start < current.start { - current = current.left - } else { - current = current.right - } - } + log("Iterator: start=", start, "end=", end, "totalSize=", l.totalSize) - if current == nil { + // For empty list or invalid range + if l == nil || l.totalSize == 0 || start < 0 || end < 0 { + log("Iterator: empty list or invalid range") return false } - if forward { - // Forward iteration - for current != nil && current.start <= end { - startIdx := max(0, start-current.start) - endIdx := min(len(current.data), end-current.start+1) - - for i := startIdx; i < endIdx; i++ { - if current.data[i] != nil { - if fn(Entry{Index: current.start + i, Value: current.data[i]}) { - return true - } + // Handle reverse iteration + if start > end { + log("Iterator: reverse mode") + count := 0 + for i := start; i >= end; i-- { + val := l.Get(i) + log("Iterator reverse: i=", i, "val=", val) + if val != nil { + count++ + if cb(i, val) { + log("Iterator reverse: callback returned false, stopping after", count, "items") + return true } } - current = current.right - if current != nil { - start = current.start - } - } - } else { - // Reverse iteration - current = l.root - for current != nil { - if end >= current.start && end < current.end { - break - } - if end < current.start { - current = current.left - } else { - current = current.right - } } + log("Iterator reverse: completed with", count, "items") + return false + } - for current != nil && current.end > start { - startIdx := min(len(current.data)-1, end-current.start) - endIdx := max(0, start-current.start) - - for i := startIdx; i >= endIdx; i-- { - if current.data[i] != nil { - if fn(Entry{Index: current.start + i, Value: current.data[i]}) { - return true - } - } - } - current = current.left - if current != nil { - end = current.end - 1 + // Handle forward iteration + log("Iterator: forward mode") + count := 0 + for i := start; i <= end; i++ { + val := l.Get(i) + log("Iterator forward: i=", i, "val=", val) + if val != nil { + count++ + if cb(i, val) { + log("Iterator forward: callback returned false, stopping after", count, "items") + return true } } } - + log("Iterator forward: completed with", count, "items") return false } - -// Helper function to get absolute value -func abs(x int) int { - if x < 0 { - return -x - } - return x -} - -// Helper functions -func max(a, b int) int { - if a > b { - return a - } - return b -} - -func min(a, b int) int { - if a < b { - return a - } - return b -} - -// Entry represents a list element with its index and value -type Entry struct { - Index int - Value interface{} -} - -// GetRange returns a slice of entries between start and end indices (inclusive). -// The direction is determined by the relationship between start and end -// (start <= end for forward iteration). Only non-nil elements are included. -func (l *List) GetRange(start, end int) []Entry { - var entries []Entry - l.Iterate(start, end, func(entry Entry) bool { - entries = append(entries, entry) - return false - }) - return entries -} - -// GetByOffset returns a slice of entries starting from the given offset, -// processing count elements in the specified direction (positive count for -// forward, negative for reverse). Only non-nil elements are included. -func (l *List) GetByOffset(offset int, count int) []Entry { - var entries []Entry - l.IterateByOffset(offset, count, func(entry Entry) bool { - entries = append(entries, entry) - return false - }) - return entries -} diff --git a/examples/gno.land/p/moul/ulist/ulist_test.gno b/examples/gno.land/p/moul/ulist/ulist_test.gno index bd0e00a7dc5..d7e96973b94 100644 --- a/examples/gno.land/p/moul/ulist/ulist_test.gno +++ b/examples/gno.land/p/moul/ulist/ulist_test.gno @@ -4,1149 +4,486 @@ import ( "testing" "gno.land/p/demo/uassert" + "gno.land/p/demo/ufmt" "gno.land/p/moul/typeutil" ) func TestNew(t *testing.T) { - tests := []struct { - name string - chunkSize int - want int - }{ - { - name: "default chunk size", - chunkSize: 0, - want: 8, - }, - { - name: "custom chunk size", - chunkSize: 16, - want: 16, - }, - { - name: "negative chunk size", - chunkSize: -1, - want: 8, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var l *List - if tt.chunkSize == 0 { - l = New() - } else { - l = NewWithSize(tt.chunkSize) - } - uassert.Equal(t, tt.want, l.chunkSize) - }) - } -} - -func TestListAppendAndGet(t *testing.T) { - tests := []struct { - name string - appendValues []interface{} - getIndex int - expectedValue interface{} - }{ - { - name: "empty list", - appendValues: nil, - getIndex: 0, - expectedValue: nil, - }, - { - name: "single append and get", - appendValues: []interface{}{42}, - getIndex: 0, - expectedValue: 42, - }, - { - name: "multiple appends and get first", - appendValues: []interface{}{1, 2, 3}, - getIndex: 0, - expectedValue: 1, - }, - { - name: "multiple appends and get last", - appendValues: []interface{}{1, 2, 3}, - getIndex: 2, - expectedValue: 3, - }, - { - name: "multiple appends and get middle", - appendValues: []interface{}{1, 2, 3}, - getIndex: 1, - expectedValue: 2, - }, - { - name: "get with negative index", - appendValues: []interface{}{1}, - getIndex: -1, - expectedValue: nil, - }, - { - name: "get beyond size", - appendValues: []interface{}{1}, - getIndex: 1, - expectedValue: nil, - }, - { - name: "append different types", - appendValues: []interface{}{42, "hello", true}, - getIndex: 1, - expectedValue: "hello", - }, - { - name: "append more than chunk size", - appendValues: []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, - getIndex: 8, - expectedValue: 9, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - l := New() - if tt.appendValues != nil { - l.Append(tt.appendValues...) - } - got := l.Get(tt.getIndex) - uassert.Equal(t, tt.expectedValue, got) - }) - } -} - -func TestListDelete(t *testing.T) { - tests := []struct { - name string - setup func() *List - deleteIndices []int - expectedErr error - expectedSize int - }{ - { - name: "delete single element", - setup: func() *List { - l := New() - l.Append(1, 2, 3) - return l - }, - deleteIndices: []int{1}, - expectedErr: nil, - expectedSize: 2, - }, - { - name: "delete multiple elements", - setup: func() *List { - l := New() - l.Append(1, 2, 3, 4, 5) - return l - }, - deleteIndices: []int{0, 2, 4}, - expectedErr: nil, - expectedSize: 2, - }, - { - name: "delete with negative index", - setup: func() *List { - l := New() - l.Append(1) - return l - }, - deleteIndices: []int{-1}, - expectedErr: ErrOutOfBounds, - expectedSize: 1, - }, - { - name: "delete beyond size", - setup: func() *List { - l := New() - l.Append(1) - return l - }, - deleteIndices: []int{1}, - expectedErr: ErrOutOfBounds, - expectedSize: 1, - }, - { - name: "delete already deleted element", - setup: func() *List { - l := New() - l.Append(1) - l.Delete(0) - return l - }, - deleteIndices: []int{0}, - expectedErr: ErrDeleted, - expectedSize: 0, - }, - { - name: "delete from empty list", - setup: func() *List { return New() }, - deleteIndices: []int{0}, - expectedErr: ErrOutOfBounds, - expectedSize: 0, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - l := tt.setup() - err := l.Delete(tt.deleteIndices...) - - if err != nil && tt.expectedErr != nil { - uassert.Equal(t, tt.expectedErr.Error(), err.Error()) - } else { - uassert.Equal(t, tt.expectedErr, err) - } - - uassert.Equal(t, tt.expectedSize, l.Size()) - }) - } -} - -func TestListSizeAndTotalSize(t *testing.T) { - tests := []struct { - name string - setup func() *List - expectedSize int - expectedTotal int - }{ - { - name: "empty list", - setup: func() *List { return New() }, - expectedSize: 0, - expectedTotal: 0, - }, - { - name: "list with elements", - setup: func() *List { - l := New() - l.Append(1, 2, 3) - return l - }, - expectedSize: 3, - expectedTotal: 3, - }, - { - name: "list with deleted elements", - setup: func() *List { - l := New() - l.Append(1, 2, 3, 4, 5) - l.Delete(1, 3) - return l - }, - expectedSize: 3, - expectedTotal: 5, - }, - { - name: "list with all elements deleted", - setup: func() *List { - l := New() - l.Append(1, 2) - l.Delete(0, 1) - return l - }, - expectedSize: 0, - expectedTotal: 2, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - l := tt.setup() - uassert.Equal(t, tt.expectedSize, l.Size()) - uassert.Equal(t, tt.expectedTotal, l.TotalSize()) - }) - } -} - -func TestListIterate(t *testing.T) { - tests := []struct { - name string - setup func() *List - start int - end int - stopEarly bool - expectedStop bool - expectedItems string - }{ - { - name: "basic forward iteration", - setup: func() *List { - l := New() - l.Append(0, 1, 2, 3, 4) - return l - }, - start: 0, - end: 4, - expectedStop: false, - expectedItems: "[0 1 2 3 4]", - }, - { - name: "basic reverse iteration", - setup: func() *List { - l := New() - l.Append(0, 1, 2, 3, 4) - return l - }, - start: 4, - end: 0, - expectedStop: false, - expectedItems: "[4 3 2 1 0]", - }, - { - name: "empty list", - setup: func() *List { - return New() - }, - start: 0, - end: 0, - expectedStop: false, - expectedItems: "[]", - }, - { - name: "negative start", - setup: func() *List { - l := New() - l.Append(1) - return l - }, - start: -1, - end: 0, - expectedStop: false, - expectedItems: "[1]", - }, - { - name: "negative end", - setup: func() *List { - l := New() - l.Append(1, 2) - return l - }, - start: 0, - end: -1, - expectedStop: false, - expectedItems: "[1]", - }, - { - name: "end beyond size", - setup: func() *List { - l := New() - l.Append(1, 2) - return l - }, - start: 0, - end: 5, - expectedStop: false, - expectedItems: "[1 2]", - }, - { - name: "stop iteration early", - setup: func() *List { - l := New() - l.Append(1, 2, 3) - return l - }, - start: 0, - end: 2, - stopEarly: true, - expectedStop: true, - expectedItems: "[1]", - }, - { - name: "sparse list with deletions", - setup: func() *List { - l := New() - for i := 0; i < 5; i++ { - l.Append(i) - } - l.Delete(1) - l.Delete(3) - return l - }, - start: 0, - end: 4, - expectedStop: false, - expectedItems: "[0 2 4]", - }, - { - name: "reverse sparse list", - setup: func() *List { - l := New() - for i := 0; i < 5; i++ { - l.Append(i) - } - l.Delete(1) - l.Delete(3) - return l - }, - start: 4, - end: 0, - expectedStop: false, - expectedItems: "[4 2 0]", - }, - { - name: "single element", - setup: func() *List { - l := New() - l.Append(1) - return l - }, - start: 0, - end: 0, - expectedStop: false, - expectedItems: "[1]", - }, - { - name: "start equals end", - setup: func() *List { - l := New() - l.Append(1, 2, 3) - return l - }, - start: 1, - end: 1, - expectedStop: false, - expectedItems: "[2]", - }, - { - name: "large list iteration", - setup: func() *List { - l := New() - for i := 0; i < 100; i++ { - l.Append(i) - } - return l - }, - start: 50, - end: 55, - expectedStop: false, - expectedItems: "[50 51 52 53 54 55]", - }, - { - name: "all elements deleted", - setup: func() *List { - l := New() - l.Append(1, 2, 3) - l.Delete(0, 1, 2) - return l - }, - start: 0, - end: 2, - expectedStop: false, - expectedItems: "[]", - }, - { - name: "alternating deletions", - setup: func() *List { - l := New() - for i := 0; i < 6; i++ { - l.Append(i) - } - l.Delete(0, 2, 4) - return l - }, - start: 0, - end: 5, - expectedStop: false, - expectedItems: "[1 3 5]", - }, - { - name: "iterate with nil values in middle", - setup: func() *List { - l := New() - l.Append(1, 2, 3, 4, 5) - l.Delete(2) - return l - }, - start: 0, - end: 4, - expectedStop: false, - expectedItems: "[1 2 4 5]", - }, - { - name: "iterate with multiple consecutive nil values", - setup: func() *List { - l := New() - l.Append(1, 2, 3, 4, 5) - l.Delete(1, 2, 3) - return l - }, - start: 0, - end: 4, - expectedStop: false, - expectedItems: "[1 5]", - }, - { - name: "iterate across chunk boundaries with deletions", - setup: func() *List { - l := NewWithSize(2) // Small chunk size to force boundaries - l.Append(0, 1, 2, 3, 4, 5) - l.Delete(1, 3, 5) - return l - }, - start: 0, - end: 5, - expectedStop: false, - expectedItems: "[0 2 4]", - }, - { - name: "iterate with start > totalSize", - setup: func() *List { - l := New() - l.Append(1, 2, 3) - return l - }, - start: 5, - end: 8, - expectedStop: false, - expectedItems: "[]", - }, - { - name: "iterate with equal start and end at deleted element", - setup: func() *List { - l := New() - l.Append(1, 2, 3) - l.Delete(1) - return l - }, - start: 1, - end: 1, - expectedStop: false, - expectedItems: "[]", - }, - { - name: "iterate with very large negative indices", - setup: func() *List { - l := New() - l.Append(1, 2, 3) - return l - }, - start: -1000, - end: -500, - expectedStop: false, - expectedItems: "[]", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - l := tt.setup() - var results []Entry - - stopped := l.Iterate(tt.start, tt.end, func(entry Entry) bool { - results = append(results, entry) - return tt.stopEarly - }) - - // Convert results to just values for comparison with existing tests - values := make([]interface{}, len(results)) - for i, entry := range results { - values[i] = entry.Value - } - - uassert.Equal(t, tt.expectedStop, stopped) - uassert.Equal(t, tt.expectedItems, typeutil.ToString(values)) - }) - } + l := New() + uassert.Equal(t, 0, l.Size()) + uassert.Equal(t, 0, l.TotalSize()) } -func TestListIterateByOffset(t *testing.T) { +func TestListAppendAndGet(t *testing.T) { tests := []struct { - name string - setup func() *List - offset int - count int - stopEarly bool - expectedStop bool - expectedItems string + name string + setup func() *List + index int + expected interface{} }{ - { - name: "basic forward iteration", - setup: func() *List { - l := New() - l.Append(0, 1, 2, 3, 4) - return l - }, - offset: 0, - count: 5, - expectedStop: false, - expectedItems: "[0 1 2 3 4]", - }, - { - name: "basic reverse iteration", - setup: func() *List { - l := New() - l.Append(0, 1, 2, 3, 4) - return l - }, - offset: 4, - count: -5, - expectedStop: false, - expectedItems: "[4 3 2 1 0]", - }, { name: "empty list", setup: func() *List { return New() }, - offset: 0, - count: 1, - expectedStop: false, - expectedItems: "[]", - }, - { - name: "stop iteration early", - setup: func() *List { - l := New() - l.Append(1, 2, 3) - return l - }, - offset: 0, - count: 3, - stopEarly: true, - expectedStop: true, - expectedItems: "[1]", - }, - { - name: "sparse list forward", - setup: func() *List { - l := New() - for i := 0; i < 5; i++ { - l.Append(i) - } - l.Delete(1) - l.Delete(3) - return l - }, - offset: 0, - count: 5, - expectedStop: false, - expectedItems: "[0 2 4]", + index: 0, + expected: nil, }, { - name: "sparse list reverse", + name: "single append and get", setup: func() *List { l := New() - for i := 0; i < 5; i++ { - l.Append(i) - } - l.Delete(1) - l.Delete(3) + l.Append(42) return l }, - offset: 4, - count: -5, - expectedStop: false, - expectedItems: "[4 2 0]", + index: 0, + expected: 42, }, { - name: "negative offset with positive diff", + name: "multiple appends and get first", setup: func() *List { l := New() l.Append(1) + l.Append(2) + l.Append(3) return l }, - offset: -1, - count: 2, - expectedStop: false, - expectedItems: "[1]", + index: 0, + expected: 1, }, { - name: "negative offset with zero diff", + name: "multiple appends and get last", setup: func() *List { l := New() l.Append(1) + l.Append(2) + l.Append(3) return l }, - offset: -1, - count: 1, - expectedStop: false, - expectedItems: "[]", + index: 2, + expected: 3, }, { - name: "zero count", + name: "get with invalid index", setup: func() *List { l := New() - l.Append(1, 2) + l.Append(1) return l }, - offset: 0, - count: 0, - expectedStop: false, - expectedItems: "[]", + index: 1, + expected: nil, }, { - name: "offset beyond size", + name: "31 items get first", setup: func() *List { l := New() - l.Append(1, 2) + for i := 0; i < 31; i++ { + l.Append(i) + } return l }, - offset: 3, - count: 1, - expectedStop: false, - expectedItems: "[]", + index: 0, + expected: 0, }, { - name: "large list with deletions forward", + name: "31 items get last", setup: func() *List { l := New() - for i := 0; i < 100; i++ { + for i := 0; i < 31; i++ { l.Append(i) } - for i := 0; i < 100; i += 2 { - l.Delete(i) - } return l }, - offset: 20, - count: 6, - expectedStop: false, - expectedItems: "[21 23 25 27 29 31]", + index: 30, + expected: 30, }, { - name: "large list with deletions reverse", + name: "31 items get middle", setup: func() *List { l := New() - for i := 0; i < 100; i++ { + for i := 0; i < 31; i++ { l.Append(i) } - for i := 0; i < 100; i += 2 { - l.Delete(i) - } return l }, - offset: 51, - count: -6, - expectedStop: false, - expectedItems: "[51 49 47 45 43 41]", + index: 15, + expected: 15, }, { - name: "all elements deleted", + name: "values around power of 2 boundary", setup: func() *List { l := New() - l.Append(1, 2, 3) - l.Delete(0, 1, 2) + for i := 0; i < 18; i++ { + l.Append(i) + } return l }, - offset: 0, - count: 3, - expectedStop: false, - expectedItems: "[]", + index: 15, + expected: 15, }, { - name: "partial range with gaps forward", + name: "values at power of 2", setup: func() *List { l := New() - for i := 0; i < 10; i++ { + for i := 0; i < 18; i++ { l.Append(i) } - l.Delete(2) - l.Delete(3) - l.Delete(6) return l }, - offset: 2, - count: 5, - expectedStop: false, - expectedItems: "[4 5 7 8 9]", + index: 16, + expected: 16, }, { - name: "partial range with gaps reverse", + name: "values after power of 2", setup: func() *List { l := New() - for i := 0; i < 10; i++ { + for i := 0; i < 18; i++ { l.Append(i) } - l.Delete(5) - l.Delete(7) - l.Delete(8) return l }, - offset: 8, - count: -5, - expectedStop: false, - expectedItems: "[6 4 3 2 1]", + index: 17, + expected: 17, }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := tt.setup() + got := l.Get(tt.index) + if got != tt.expected { + t.Errorf("List.Get() = %v, want %v", got, tt.expected) + } + }) + } +} + +// generateSequence creates a slice of integers from 0 to n-1 +func generateSequence(n int) []interface{} { + result := make([]interface{}, n) + for i := 0; i < n; i++ { + result[i] = i + } + return result +} + +func TestListDelete(t *testing.T) { + tests := []struct { + name string + setup func() *List + deleteIndices []int + expectedErr error + expectedSize int + }{ { - name: "offset at last element forward", + name: "delete single element", setup: func() *List { l := New() l.Append(1, 2, 3) return l }, - offset: 2, - count: 1, - expectedStop: false, - expectedItems: "[3]", + deleteIndices: []int{1}, + expectedErr: nil, + expectedSize: 2, }, { - name: "offset at first element reverse", + name: "delete multiple elements", setup: func() *List { l := New() - l.Append(1, 2, 3) + l.Append(1, 2, 3, 4, 5) return l }, - offset: 0, - count: -1, - expectedStop: false, - expectedItems: "[1]", + deleteIndices: []int{0, 2, 4}, + expectedErr: nil, + expectedSize: 2, }, { - name: "count exceeds list bounds forward", + name: "delete with negative index", setup: func() *List { l := New() - l.Append(1, 2, 3) + l.Append(1) return l }, - offset: 1, - count: 100, - expectedStop: false, - expectedItems: "[2 3]", + deleteIndices: []int{-1}, + expectedErr: ErrOutOfBounds, + expectedSize: 1, }, { - name: "count exceeds list bounds reverse", + name: "delete beyond size", setup: func() *List { l := New() - l.Append(1, 2, 3) + l.Append(1) return l }, - offset: 1, - count: -100, - expectedStop: false, - expectedItems: "[2 1]", + deleteIndices: []int{1}, + expectedErr: ErrOutOfBounds, + expectedSize: 1, }, { - name: "single element list forward", + name: "delete already deleted element", setup: func() *List { l := New() - l.Append(42) + l.Append(1) + l.Delete(0) return l }, - offset: 0, - count: 1, - expectedStop: false, - expectedItems: "[42]", + deleteIndices: []int{0}, + expectedErr: ErrDeleted, + expectedSize: 0, }, { - name: "single element list reverse", + name: "delete multiple elements in reverse", setup: func() *List { l := New() - l.Append(42) - return l - }, - offset: 0, - count: -1, - expectedStop: false, - expectedItems: "[42]", - }, - { - name: "iterate exactly one chunk size forward", - setup: func() *List { - l := NewWithSize(4) - l.Append(0, 1, 2, 3) - return l - }, - offset: 0, - count: 4, - expectedStop: false, - expectedItems: "[0 1 2 3]", - }, - { - name: "iterate exactly one chunk size reverse", - setup: func() *List { - l := NewWithSize(4) - l.Append(0, 1, 2, 3) - return l - }, - offset: 3, - count: -4, - expectedStop: false, - expectedItems: "[3 2 1 0]", - }, - { - name: "iterate across chunk boundary forward", - setup: func() *List { - l := NewWithSize(3) - l.Append(0, 1, 2, 3, 4) // Creates two chunks: [0,1,2] [3,4] - return l - }, - offset: 1, - count: 3, - expectedStop: false, - expectedItems: "[1 2 3]", - }, - { - name: "iterate across chunk boundary reverse", - setup: func() *List { - l := NewWithSize(3) - l.Append(0, 1, 2, 3, 4) // Creates two chunks: [0,1,2] [3,4] - return l - }, - offset: 3, - count: -3, - expectedStop: false, - expectedItems: "[3 2 1]", - }, - { - name: "iterate with deletions at chunk boundaries forward", - setup: func() *List { - l := NewWithSize(3) - l.Append(0, 1, 2, 3, 4, 5) // Creates two chunks: [0,1,2] [3,4,5] - l.Delete(2, 3) // Delete elements at chunk boundaries - return l - }, - offset: 0, - count: 6, - expectedStop: false, - expectedItems: "[0 1 4 5]", - }, - { - name: "iterate with deletions at chunk boundaries reverse", - setup: func() *List { - l := NewWithSize(3) - l.Append(0, 1, 2, 3, 4, 5) // Creates two chunks: [0,1,2] [3,4,5] - l.Delete(2, 3) // Delete elements at chunk boundaries - return l - }, - offset: 5, - count: -6, - expectedStop: false, - expectedItems: "[5 4 1 0]", - }, - { - name: "iterate starting at last element of chunk", - setup: func() *List { - l := NewWithSize(3) - l.Append(0, 1, 2, 3, 4) // Creates two chunks: [0,1,2] [3,4] - return l - }, - offset: 2, - count: 2, - expectedStop: false, - expectedItems: "[2 3]", - }, - { - name: "iterate with multiple full chunks", - setup: func() *List { - l := NewWithSize(2) - l.Append(0, 1, 2, 3, 4, 5) // Creates three chunks: [0,1] [2,3] [4,5] - return l - }, - offset: 1, - count: 4, - expectedStop: false, - expectedItems: "[1 2 3 4]", - }, - { - name: "iterate with partially filled last chunk", - setup: func() *List { - l := NewWithSize(3) - l.Append(0, 1, 2, 3) // Creates two chunks: [0,1,2] [3] + l.Append(1, 2, 3, 4, 5) return l }, - offset: 2, - count: 2, - expectedStop: false, - expectedItems: "[2 3]", + deleteIndices: []int{4, 2, 0}, + expectedErr: nil, + expectedSize: 2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { l := tt.setup() - var results []Entry - - stopped := l.IterateByOffset(tt.offset, tt.count, func(entry Entry) bool { - results = append(results, entry) - return tt.stopEarly - }) - - // Convert results to just values for comparison with existing tests - values := make([]interface{}, len(results)) - for i, entry := range results { - values[i] = entry.Value + initialSize := l.Size() + err := l.Delete(tt.deleteIndices...) + if err != nil && tt.expectedErr != nil { + uassert.Equal(t, tt.expectedErr.Error(), err.Error()) + } else { + uassert.Equal(t, tt.expectedErr, err) } - - uassert.Equal(t, tt.expectedStop, stopped) - uassert.Equal(t, tt.expectedItems, typeutil.ToString(values)) + uassert.Equal(t, tt.expectedSize, l.Size(), + ufmt.Sprintf("Expected size %d after deleting %d elements from size %d, got %d", + tt.expectedSize, len(tt.deleteIndices), initialSize, l.Size())) }) } } -func TestListGetRange(t *testing.T) { +func TestListSizeAndTotalSize(t *testing.T) { + t.Run("empty list", func(t *testing.T) { + list := New() + uassert.Equal(t, 0, list.Size()) + uassert.Equal(t, 0, list.TotalSize()) + }) + + t.Run("list with elements", func(t *testing.T) { + list := New() + list.Append(1) + list.Append(2) + list.Append(3) + uassert.Equal(t, 3, list.Size()) + uassert.Equal(t, 3, list.TotalSize()) + }) + + t.Run("list with deleted elements", func(t *testing.T) { + list := New() + list.Append(1) + list.Append(2) + list.Append(3) + list.Delete(1) + uassert.Equal(t, 2, list.Size()) + uassert.Equal(t, 3, list.TotalSize()) + }) +} + +func TestIterator(t *testing.T) { tests := []struct { - name string - setup func() *List - start int - end int - expectedItems string + name string + values []interface{} + start int + end int + expected []Entry + wantStop bool + stopAfter int // stop after N elements, -1 for no stop }{ { - name: "basic forward range", - setup: func() *List { - l := New() - l.Append(0, 1, 2, 3, 4) - return l - }, - start: 0, - end: 4, - expectedItems: "[0 1 2 3 4]", + name: "empty list", + values: []interface{}{}, + start: 0, + end: 10, + expected: []Entry{}, + stopAfter: -1, + }, + { + name: "nil list", + values: nil, + start: 0, + end: 0, + expected: []Entry{}, + stopAfter: -1, }, { - name: "basic reverse range", - setup: func() *List { - l := New() - l.Append(0, 1, 2, 3, 4) - return l + name: "single element forward", + values: []interface{}{42}, + start: 0, + end: 0, + expected: []Entry{ + {Index: 0, Value: 42}, }, - start: 4, - end: 0, - expectedItems: "[4 3 2 1 0]", + stopAfter: -1, }, { - name: "range with deletions", - setup: func() *List { - l := New() - l.Append(0, 1, 2, 3, 4) - l.Delete(1, 3) - return l + name: "multiple elements forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + {Index: 4, Value: 5}, }, - start: 0, - end: 4, - expectedItems: "[0 2 4]", + stopAfter: -1, + }, + { + name: "multiple elements reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 4, + end: 0, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, + {Index: 0, Value: 1}, + }, + stopAfter: -1, + }, + { + name: "partial range forward", + values: []interface{}{1, 2, 3, 4, 5}, + start: 1, + end: 3, + expected: []Entry{ + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + {Index: 3, Value: 4}, + }, + stopAfter: -1, }, { - name: "empty list", - setup: func() *List { - return New() + name: "partial range reverse", + values: []interface{}{1, 2, 3, 4, 5}, + start: 3, + end: 1, + expected: []Entry{ + {Index: 3, Value: 4}, + {Index: 2, Value: 3}, + {Index: 1, Value: 2}, }, - start: 0, - end: 4, - expectedItems: "[]", + stopAfter: -1, }, { - name: "single element", - setup: func() *List { - l := New() - l.Append(42) - return l + name: "stop iteration early", + values: []interface{}{1, 2, 3, 4, 5}, + start: 0, + end: 4, + wantStop: true, + stopAfter: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, }, - start: 0, - end: 0, - expectedItems: "[42]", }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - l := tt.setup() - entries := l.GetRange(tt.start, tt.end) - - // Convert entries to values for comparison - values := make([]interface{}, len(entries)) - for i, entry := range entries { - values[i] = entry.Value - } - - uassert.Equal(t, tt.expectedItems, typeutil.ToString(values)) - }) - } -} - -func TestListGetByOffset(t *testing.T) { - tests := []struct { - name string - setup func() *List - offset int - count int - expectedItems string - }{ { - name: "basic forward offset", - setup: func() *List { - l := New() - l.Append(0, 1, 2, 3, 4) - return l + name: "negative start", + values: []interface{}{1, 2, 3}, + start: -1, + end: 2, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, }, - offset: 1, - count: 3, - expectedItems: "[1 2 3]", + stopAfter: -1, + }, + { + name: "negative end", + values: []interface{}{1, 2, 3}, + start: 0, + end: -2, + expected: []Entry{ + {Index: 0, Value: 1}, + }, + stopAfter: -1, }, { - name: "basic reverse offset", - setup: func() *List { - l := New() - l.Append(0, 1, 2, 3, 4) - return l - }, - offset: 3, - count: -3, - expectedItems: "[3 2 1]", + name: "start beyond size", + values: []interface{}{1, 2, 3}, + start: 5, + end: 6, + expected: []Entry{}, + stopAfter: -1, }, { - name: "offset with deletions", - setup: func() *List { - l := New() - l.Append(0, 1, 2, 3, 4) - l.Delete(1, 3) - return l - }, - offset: 0, - count: 5, - expectedItems: "[0 2 4]", + name: "end beyond size", + values: []interface{}{1, 2, 3}, + start: 0, + end: 5, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 2, Value: 3}, + }, + stopAfter: -1, }, { - name: "offset beyond size", - setup: func() *List { - l := New() - l.Append(0, 1, 2) - return l + name: "with deleted elements", + values: []interface{}{1, 2, nil, 4, 5}, + start: 0, + end: 4, + expected: []Entry{ + {Index: 0, Value: 1}, + {Index: 1, Value: 2}, + {Index: 3, Value: 4}, + {Index: 4, Value: 5}, }, - offset: 5, - count: 2, - expectedItems: "[]", + stopAfter: -1, }, { - name: "negative offset with positive count", - setup: func() *List { - l := New() - l.Append(0, 1, 2) - return l + name: "with deleted elements reverse", + values: []interface{}{1, nil, 3, nil, 5}, + start: 4, + end: 0, + expected: []Entry{ + {Index: 4, Value: 5}, + {Index: 2, Value: 3}, + {Index: 0, Value: 1}, }, - offset: -1, - count: 2, - expectedItems: "[0]", + stopAfter: -1, }, { - name: "zero count", - setup: func() *List { - l := New() - l.Append(0, 1, 2) - return l - }, - offset: 0, - count: 0, - expectedItems: "[]", + name: "start equals end", + values: []interface{}{1, 2, 3}, + start: 1, + end: 1, + expected: []Entry{{Index: 1, Value: 2}}, + stopAfter: -1, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - l := tt.setup() - entries := l.GetByOffset(tt.offset, tt.count) + list := New() + list.Append(tt.values...) + + var result []Entry + stopped := list.Iterator(tt.start, tt.end, func(index int, value interface{}) bool { + result = append(result, Entry{Index: index, Value: value}) + return tt.stopAfter >= 0 && len(result) >= tt.stopAfter + }) + + uassert.Equal(t, len(result), len(tt.expected), "comparing length") - // Convert entries to values for comparison - values := make([]interface{}, len(entries)) - for i, entry := range entries { - values[i] = entry.Value + for i := range result { + uassert.Equal(t, result[i].Index, tt.expected[i].Index, "comparing index") + uassert.Equal(t, typeutil.ToString(result[i].Value), typeutil.ToString(tt.expected[i].Value), "comparing value") } - uassert.Equal(t, tt.expectedItems, typeutil.ToString(values)) + uassert.Equal(t, stopped, tt.wantStop, "comparing stopped") }) } }