feat: add data structures from utils (iterator, buffer, sampler, etc)

This commit is contained in:
2026-01-09 15:21:03 -08:00
parent 1b5dc76556
commit d4cc76bbbc
46 changed files with 5365 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bimap
import (
"bytes"
"encoding/json"
"errors"
"maps"
"slices"
"github.com/luxfi/utils"
)
var (
_ json.Marshaler = (*BiMap[int, int])(nil)
_ json.Unmarshaler = (*BiMap[int, int])(nil)
nullBytes = []byte("null")
errNotBijective = errors.New("map not bijective")
)
type Entry[K, V any] struct {
Key K
Value V
}
// BiMap is a bi-directional map.
type BiMap[K, V comparable] struct {
keyToValue map[K]V
valueToKey map[V]K
}
// New creates a new empty bimap.
func New[K, V comparable]() *BiMap[K, V] {
return &BiMap[K, V]{
keyToValue: make(map[K]V),
valueToKey: make(map[V]K),
}
}
// Put the key value pair into the map. If either [key] or [val] was previously
// in the map, the previous entries will be removed and returned.
//
// Note: Unlike normal maps, it's possible that Put removes 0, 1, or 2 existing
// entries to ensure that mappings are one-to-one.
func (m *BiMap[K, V]) Put(key K, val V) []Entry[K, V] {
var removed []Entry[K, V]
oldVal, oldValDeleted := m.DeleteKey(key)
if oldValDeleted {
removed = append(removed, Entry[K, V]{
Key: key,
Value: oldVal,
})
}
oldKey, oldKeyDeleted := m.DeleteValue(val)
if oldKeyDeleted {
removed = append(removed, Entry[K, V]{
Key: oldKey,
Value: val,
})
}
m.keyToValue[key] = val
m.valueToKey[val] = key
return removed
}
// GetKey that maps to the provided value.
func (m *BiMap[K, V]) GetKey(val V) (K, bool) {
key, ok := m.valueToKey[val]
return key, ok
}
// GetValue that is mapped to the provided key.
func (m *BiMap[K, V]) GetValue(key K) (V, bool) {
val, ok := m.keyToValue[key]
return val, ok
}
// HasKey returns true if [key] is in the map.
func (m *BiMap[K, _]) HasKey(key K) bool {
_, ok := m.keyToValue[key]
return ok
}
// HasValue returns true if [val] is in the map.
func (m *BiMap[_, V]) HasValue(val V) bool {
_, ok := m.valueToKey[val]
return ok
}
// DeleteKey removes [key] from the map and returns the value it mapped to.
func (m *BiMap[K, V]) DeleteKey(key K) (V, bool) {
val, ok := m.keyToValue[key]
if !ok {
return utils.Zero[V](), false
}
delete(m.keyToValue, key)
delete(m.valueToKey, val)
return val, true
}
// DeleteValue removes [val] from the map and returns the key that mapped to it.
func (m *BiMap[K, V]) DeleteValue(val V) (K, bool) {
key, ok := m.valueToKey[val]
if !ok {
return utils.Zero[K](), false
}
delete(m.keyToValue, key)
delete(m.valueToKey, val)
return key, true
}
// Keys returns the keys of the map. The keys will be in an indeterminate order.
func (m *BiMap[K, _]) Keys() []K {
return slices.Collect(maps.Keys(m.keyToValue))
}
// Values returns the values of the map. The values will be in an indeterminate
// order.
func (m *BiMap[_, V]) Values() []V {
return slices.Collect(maps.Values(m.keyToValue))
}
// Len return the number of entries in this map.
func (m *BiMap[K, V]) Len() int {
return len(m.keyToValue)
}
func (m *BiMap[K, V]) MarshalJSON() ([]byte, error) {
return json.Marshal(m.keyToValue)
}
func (m *BiMap[K, V]) UnmarshalJSON(b []byte) error {
if bytes.Equal(b, nullBytes) {
return nil
}
var keyToValue map[K]V
if err := json.Unmarshal(b, &keyToValue); err != nil {
return err
}
valueToKey := make(map[V]K, len(keyToValue))
for k, v := range keyToValue {
valueToKey[v] = k
}
if len(keyToValue) != len(valueToKey) {
return errNotBijective
}
m.keyToValue = keyToValue
m.valueToKey = valueToKey
return nil
}
+366
View File
@@ -0,0 +1,366 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package bimap
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestBiMapPut(t *testing.T) {
tests := []struct {
name string
state *BiMap[int, int]
key int
value int
expectedRemoved []Entry[int, int]
expectedState *BiMap[int, int]
}{
{
name: "none removed",
state: New[int, int](),
key: 1,
value: 2,
expectedRemoved: nil,
expectedState: &BiMap[int, int]{
keyToValue: map[int]int{
1: 2,
},
valueToKey: map[int]int{
2: 1,
},
},
},
{
name: "key removed",
state: &BiMap[int, int]{
keyToValue: map[int]int{
1: 2,
},
valueToKey: map[int]int{
2: 1,
},
},
key: 1,
value: 3,
expectedRemoved: []Entry[int, int]{
{
Key: 1,
Value: 2,
},
},
expectedState: &BiMap[int, int]{
keyToValue: map[int]int{
1: 3,
},
valueToKey: map[int]int{
3: 1,
},
},
},
{
name: "value removed",
state: &BiMap[int, int]{
keyToValue: map[int]int{
1: 2,
},
valueToKey: map[int]int{
2: 1,
},
},
key: 3,
value: 2,
expectedRemoved: []Entry[int, int]{
{
Key: 1,
Value: 2,
},
},
expectedState: &BiMap[int, int]{
keyToValue: map[int]int{
3: 2,
},
valueToKey: map[int]int{
2: 3,
},
},
},
{
name: "key and value removed",
state: &BiMap[int, int]{
keyToValue: map[int]int{
1: 2,
3: 4,
},
valueToKey: map[int]int{
2: 1,
4: 3,
},
},
key: 1,
value: 4,
expectedRemoved: []Entry[int, int]{
{
Key: 1,
Value: 2,
},
{
Key: 3,
Value: 4,
},
},
expectedState: &BiMap[int, int]{
keyToValue: map[int]int{
1: 4,
},
valueToKey: map[int]int{
4: 1,
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
removed := test.state.Put(test.key, test.value)
require.Equal(test.expectedRemoved, removed)
require.Equal(test.expectedState, test.state)
})
}
}
func TestBiMapHasValueAndGetKey(t *testing.T) {
m := New[int, int]()
require.Empty(t, m.Put(1, 2))
tests := []struct {
name string
value int
expectedKey int
expectedExists bool
}{
{
name: "fetch unknown",
value: 3,
expectedKey: 0,
expectedExists: false,
},
{
name: "fetch known value",
value: 2,
expectedKey: 1,
expectedExists: true,
},
{
name: "fetch known key",
value: 1,
expectedKey: 0,
expectedExists: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
exists := m.HasValue(test.value)
require.Equal(test.expectedExists, exists)
key, exists := m.GetKey(test.value)
require.Equal(test.expectedKey, key)
require.Equal(test.expectedExists, exists)
})
}
}
func TestBiMapHasKeyAndGetValue(t *testing.T) {
m := New[int, int]()
require.Empty(t, m.Put(1, 2))
tests := []struct {
name string
key int
expectedValue int
expectedExists bool
}{
{
name: "fetch unknown",
key: 3,
expectedValue: 0,
expectedExists: false,
},
{
name: "fetch known key",
key: 1,
expectedValue: 2,
expectedExists: true,
},
{
name: "fetch known value",
key: 2,
expectedValue: 0,
expectedExists: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
exists := m.HasKey(test.key)
require.Equal(test.expectedExists, exists)
value, exists := m.GetValue(test.key)
require.Equal(test.expectedValue, value)
require.Equal(test.expectedExists, exists)
})
}
}
func TestBiMapDeleteKey(t *testing.T) {
tests := []struct {
name string
state *BiMap[int, int]
key int
expectedValue int
expectedRemoved bool
expectedState *BiMap[int, int]
}{
{
name: "none removed",
state: New[int, int](),
key: 1,
expectedValue: 0,
expectedRemoved: false,
expectedState: New[int, int](),
},
{
name: "key removed",
state: &BiMap[int, int]{
keyToValue: map[int]int{
1: 2,
},
valueToKey: map[int]int{
2: 1,
},
},
key: 1,
expectedValue: 2,
expectedRemoved: true,
expectedState: New[int, int](),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
value, removed := test.state.DeleteKey(test.key)
require.Equal(test.expectedValue, value)
require.Equal(test.expectedRemoved, removed)
require.Equal(test.expectedState, test.state)
})
}
}
func TestBiMapDeleteValue(t *testing.T) {
tests := []struct {
name string
state *BiMap[int, int]
value int
expectedKey int
expectedRemoved bool
expectedState *BiMap[int, int]
}{
{
name: "none removed",
state: New[int, int](),
value: 1,
expectedKey: 0,
expectedRemoved: false,
expectedState: New[int, int](),
},
{
name: "key removed",
state: &BiMap[int, int]{
keyToValue: map[int]int{
1: 2,
},
valueToKey: map[int]int{
2: 1,
},
},
value: 2,
expectedKey: 1,
expectedRemoved: true,
expectedState: New[int, int](),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
key, removed := test.state.DeleteValue(test.value)
require.Equal(test.expectedKey, key)
require.Equal(test.expectedRemoved, removed)
require.Equal(test.expectedState, test.state)
})
}
}
func TestBiMapLenAndLists(t *testing.T) {
require := require.New(t)
m := New[int, int]()
require.Zero(m.Len())
require.Empty(m.Keys())
require.Empty(m.Values())
m.Put(1, 2)
require.Equal(1, m.Len())
require.ElementsMatch([]int{1}, m.Keys())
require.ElementsMatch([]int{2}, m.Values())
m.Put(2, 3)
require.Equal(2, m.Len())
require.ElementsMatch([]int{1, 2}, m.Keys())
require.ElementsMatch([]int{2, 3}, m.Values())
m.Put(1, 3)
require.Equal(1, m.Len())
require.ElementsMatch([]int{1}, m.Keys())
require.ElementsMatch([]int{3}, m.Values())
m.DeleteKey(1)
require.Zero(m.Len())
require.Empty(m.Keys())
require.Empty(m.Values())
}
func TestBiMapJSON(t *testing.T) {
require := require.New(t)
expectedMap := New[int, int]()
expectedMap.Put(1, 2)
expectedMap.Put(2, 3)
jsonBytes, err := json.Marshal(expectedMap)
require.NoError(err)
expectedJSONBytes := []byte(`{"1":2,"2":3}`)
require.JSONEq(string(expectedJSONBytes), string(jsonBytes))
var unmarshalledMap BiMap[int, int]
require.NoError(json.Unmarshal(jsonBytes, &unmarshalledMap))
require.Equal(expectedMap, &unmarshalledMap)
}
func TestBiMapInvalidJSON(t *testing.T) {
require := require.New(t)
invalidJSONBytes := []byte(`{"1":2,"2":2}`)
var unmarshalledMap BiMap[int, int]
err := json.Unmarshal(invalidJSONBytes, &unmarshalledMap)
require.ErrorIs(err, errNotBijective)
}
+90
View File
@@ -0,0 +1,90 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package buffer
import "errors"
var (
_ Queue[struct{}] = (*boundedQueue[struct{}])(nil)
errInvalidMaxSize = errors.New("maxSize must be greater than 0")
)
// A FIFO queue.
type Queue[T any] interface {
// Pushes [elt] onto the queue.
// If the queue is full, the oldest element is evicted to make space.
Push(T)
// Pops the oldest element from the queue.
// Returns false if the queue is empty.
Pop() (T, bool)
// Returns the oldest element without removing it.
// Returns false if the queue is empty.
Peek() (T, bool)
// Returns the element at the given index without removing it.
// Index(0) returns the oldest element.
// Index(Len() - 1) returns the newest element.
// Returns false if there is no element at that index.
Index(int) (T, bool)
// Returns the number of elements in the queue.
Len() int
// Returns the queue elements from oldest to newest.
// This is an O(n) operation and should be used sparingly.
List() []T
}
// Keeps up to [maxSize] entries in an ordered buffer
// and calls [onEvict] on any item that is evicted.
// Not safe for concurrent use.
type boundedQueue[T any] struct {
deque Deque[T]
maxSize int
onEvict func(T)
}
// Returns a new bounded, non-blocking queue that holds up to [maxSize] elements.
// When an element is evicted, [onEvict] is called with the evicted element.
// If [onEvict] is nil, this is a no-op.
// [maxSize] must be >= 1.
// Not safe for concurrent use.
func NewBoundedQueue[T any](maxSize int, onEvict func(T)) (Queue[T], error) {
if maxSize < 1 {
return nil, errInvalidMaxSize
}
return &boundedQueue[T]{
deque: NewUnboundedDeque[T](maxSize + 1), // +1 so we never resize
maxSize: maxSize,
onEvict: onEvict,
}, nil
}
func (b *boundedQueue[T]) Push(elt T) {
if b.deque.Len() == b.maxSize {
evicted, _ := b.deque.PopLeft()
if b.onEvict != nil {
b.onEvict(evicted)
}
}
_ = b.deque.PushRight(elt)
}
func (b *boundedQueue[T]) Pop() (T, bool) {
return b.deque.PopLeft()
}
func (b *boundedQueue[T]) Peek() (T, bool) {
return b.deque.PeekLeft()
}
func (b *boundedQueue[T]) Index(i int) (T, bool) {
return b.deque.Index(i)
}
func (b *boundedQueue[T]) Len() int {
return b.deque.Len()
}
func (b *boundedQueue[T]) List() []T {
return b.deque.List()
}
+142
View File
@@ -0,0 +1,142 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package buffer
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestNewBoundedQueue(t *testing.T) {
require := require.New(t)
// Case: maxSize < 1
_, err := NewBoundedQueue[bool](0, nil)
require.ErrorIs(err, errInvalidMaxSize)
// Case: maxSize == 1 and nil onEvict
b, err := NewBoundedQueue[bool](1, nil)
require.NoError(err)
// Put 2 elements to make sure we don't panic on evict
b.Push(true)
b.Push(true)
}
func TestBoundedQueue(t *testing.T) {
require := require.New(t)
maxSize := 3
evicted := []int{}
onEvict := func(elt int) {
evicted = append(evicted, elt)
}
b, err := NewBoundedQueue(maxSize, onEvict)
require.NoError(err)
require.Zero(b.Len())
// Fill the queue
for i := 0; i < maxSize; i++ {
b.Push(i)
require.Equal(i+1, b.Len())
got, ok := b.Peek()
require.True(ok)
require.Zero(got)
got, ok = b.Index(i)
require.True(ok)
require.Equal(i, got)
require.Len(b.List(), i+1)
}
require.Equal([]int{}, evicted)
require.Len(b.List(), maxSize)
// Queue is [0, 1, 2]
// Empty the queue
for i := 0; i < maxSize; i++ {
got, ok := b.Pop()
require.True(ok)
require.Equal(i, got)
require.Equal(maxSize-i-1, b.Len())
require.Len(b.List(), maxSize-i-1)
}
// Queue is empty
_, ok := b.Pop()
require.False(ok)
_, ok = b.Peek()
require.False(ok)
_, ok = b.Index(0)
require.False(ok)
require.Zero(b.Len())
require.Empty(b.List())
// Fill the queue again
for i := 0; i < maxSize; i++ {
b.Push(i)
require.Equal(i+1, b.Len())
}
// Queue is [0, 1, 2]
// Putting another element should evict the oldest.
b.Push(maxSize)
// Queue is [1, 2, 3]
require.Equal(maxSize, b.Len())
require.Len(b.List(), maxSize)
got, ok := b.Peek()
require.True(ok)
require.Equal(1, got)
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.Index(maxSize - 1)
require.True(ok)
require.Equal(maxSize, got)
require.Equal([]int{0}, evicted)
// Put 2 more elements
b.Push(maxSize + 1)
b.Push(maxSize + 2)
// Queue is [3, 4, 5]
require.Equal(maxSize, b.Len())
require.Equal([]int{0, 1, 2}, evicted)
got, ok = b.Peek()
require.True(ok)
require.Equal(3, got)
require.Equal([]int{3, 4, 5}, b.List())
for i := maxSize; i < 2*maxSize; i++ {
got, ok := b.Index(i - maxSize)
require.True(ok)
require.Equal(i, got)
}
// Empty the queue
for i := 0; i < maxSize; i++ {
got, ok := b.Pop()
require.True(ok)
require.Equal(i+3, got)
require.Equal(maxSize-i-1, b.Len())
require.Len(b.List(), maxSize-i-1)
}
// Queue is empty
require.Empty(b.List())
require.Zero(b.Len())
require.Equal([]int{0, 1, 2}, evicted)
_, ok = b.Pop()
require.False(ok)
_, ok = b.Peek()
require.False(ok)
_, ok = b.Index(0)
require.False(ok)
}
+169
View File
@@ -0,0 +1,169 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package buffer
import (
"sync"
"github.com/luxfi/utils"
)
var _ BlockingDeque[int] = (*unboundedBlockingDeque[int])(nil)
type BlockingDeque[T any] interface {
Deque[T]
// Close and empty the deque.
Close()
}
// Returns a new unbounded deque with the given initial size.
// Note that the returned deque is always empty -- [initSize] is just
// a hint to prevent unnecessary resizing.
func NewUnboundedBlockingDeque[T any](initSize int) BlockingDeque[T] {
q := &unboundedBlockingDeque[T]{
Deque: NewUnboundedDeque[T](initSize),
}
q.cond = sync.NewCond(&q.lock)
return q
}
type unboundedBlockingDeque[T any] struct {
lock sync.RWMutex
cond *sync.Cond
closed bool
Deque[T]
}
// If the deque is closed returns false.
func (q *unboundedBlockingDeque[T]) PushRight(elt T) bool {
q.cond.L.Lock()
defer q.cond.L.Unlock()
if q.closed {
return false
}
// Add the item to the queue
q.Deque.PushRight(elt)
// Signal a waiting thread
q.cond.Signal()
return true
}
// If the deque is closed returns false.
func (q *unboundedBlockingDeque[T]) PopRight() (T, bool) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
for {
if q.closed {
return utils.Zero[T](), false
}
if q.Deque.Len() != 0 {
return q.Deque.PopRight()
}
q.cond.Wait()
}
}
func (q *unboundedBlockingDeque[T]) PeekRight() (T, bool) {
q.lock.RLock()
defer q.lock.RUnlock()
if q.closed {
return utils.Zero[T](), false
}
return q.Deque.PeekRight()
}
// If the deque is closed returns false.
func (q *unboundedBlockingDeque[T]) PushLeft(elt T) bool {
q.cond.L.Lock()
defer q.cond.L.Unlock()
if q.closed {
return false
}
// Add the item to the queue
q.Deque.PushLeft(elt)
// Signal a waiting thread
q.cond.Signal()
return true
}
// If the deque is closed returns false.
func (q *unboundedBlockingDeque[T]) PopLeft() (T, bool) {
q.cond.L.Lock()
defer q.cond.L.Unlock()
for {
if q.closed {
return utils.Zero[T](), false
}
if q.Deque.Len() != 0 {
return q.Deque.PopLeft()
}
q.cond.Wait()
}
}
func (q *unboundedBlockingDeque[T]) PeekLeft() (T, bool) {
q.lock.RLock()
defer q.lock.RUnlock()
if q.closed {
return utils.Zero[T](), false
}
return q.Deque.PeekLeft()
}
func (q *unboundedBlockingDeque[T]) Index(i int) (T, bool) {
q.lock.RLock()
defer q.lock.RUnlock()
if q.closed {
return utils.Zero[T](), false
}
return q.Deque.Index(i)
}
func (q *unboundedBlockingDeque[T]) Len() int {
q.lock.RLock()
defer q.lock.RUnlock()
if q.closed {
return 0
}
return q.Deque.Len()
}
func (q *unboundedBlockingDeque[T]) List() []T {
q.lock.RLock()
defer q.lock.RUnlock()
if q.closed {
return nil
}
return q.Deque.List()
}
func (q *unboundedBlockingDeque[T]) Close() {
q.cond.L.Lock()
defer q.cond.L.Unlock()
if q.closed {
return
}
q.Deque = nil
// Mark the queue as closed
q.closed = true
q.cond.Broadcast()
}
+106
View File
@@ -0,0 +1,106 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package buffer
import (
"sync"
"testing"
"github.com/stretchr/testify/require"
)
func TestUnboundedBlockingDequePush(t *testing.T) {
require := require.New(t)
deque := NewUnboundedBlockingDeque[int](2)
require.Empty(deque.List())
_, ok := deque.Index(0)
require.False(ok)
ok = deque.PushRight(1)
require.True(ok)
require.Equal([]int{1}, deque.List())
got, ok := deque.Index(0)
require.True(ok)
require.Equal(1, got)
ok = deque.PushRight(2)
require.True(ok)
require.Equal([]int{1, 2}, deque.List())
got, ok = deque.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = deque.Index(1)
require.True(ok)
require.Equal(2, got)
_, ok = deque.Index(2)
require.False(ok)
ch, ok := deque.PopLeft()
require.True(ok)
require.Equal(1, ch)
require.Equal([]int{2}, deque.List())
got, ok = deque.Index(0)
require.True(ok)
require.Equal(2, got)
}
func TestUnboundedBlockingDequePop(t *testing.T) {
require := require.New(t)
deque := NewUnboundedBlockingDeque[int](2)
require.Empty(deque.List())
ok := deque.PushRight(1)
require.True(ok)
require.Equal([]int{1}, deque.List())
got, ok := deque.Index(0)
require.True(ok)
require.Equal(1, got)
ch, ok := deque.PopLeft()
require.True(ok)
require.Equal(1, ch)
require.Empty(deque.List())
var (
gotOk bool
gotCh int
)
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
gotCh, gotOk = deque.PopLeft()
wg.Done()
}()
ok = deque.PushRight(2)
require.True(ok)
wg.Wait()
require.True(gotOk)
require.Equal(2, gotCh)
require.Empty(deque.List())
_, ok = deque.Index(0)
require.False(ok)
}
func TestUnboundedBlockingDequeClose(t *testing.T) {
require := require.New(t)
deque := NewUnboundedBlockingDeque[int](2)
ok := deque.PushLeft(1)
require.True(ok)
deque.Close()
_, ok = deque.PopRight()
require.False(ok)
ok = deque.PushLeft(1)
require.False(ok)
}
+190
View File
@@ -0,0 +1,190 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package buffer
import "github.com/luxfi/utils"
const defaultInitSize = 32
// An unbounded deque (double-ended queue).
// See https://en.wikipedia.org/wiki/Double-ended_queue
// Not safe for concurrent access.
type Deque[T any] interface {
// Place an element at the leftmost end of the deque.
// Returns true if the element was placed in the deque.
PushLeft(T) bool
// Place an element at the rightmost end of the deque.
// Returns true if the element was placed in the deque.
PushRight(T) bool
// Remove and return the leftmost element of the deque.
// Returns false if the deque is empty.
PopLeft() (T, bool)
// Remove and return the rightmost element of the deque.
// Returns false if the deque is empty.
PopRight() (T, bool)
// Return the leftmost element of the deque without removing it.
// Returns false if the deque is empty.
PeekLeft() (T, bool)
// Return the rightmost element of the deque without removing it.
// Returns false if the deque is empty.
PeekRight() (T, bool)
// Returns the element at the given index.
// Returns false if the index is out of bounds.
// The leftmost element is at index 0.
Index(int) (T, bool)
// Returns the number of elements in the deque.
Len() int
// Returns the elements in the deque from left to right.
List() []T
}
// Returns a new unbounded deque with the given initial slice size.
// Note that the returned deque is always empty -- [initSize] is just
// a hint to prevent unnecessary resizing.
func NewUnboundedDeque[T any](initSize int) Deque[T] {
if initSize < 2 {
initSize = defaultInitSize
}
return &unboundedSliceDeque[T]{
// Note that [initSize] must be >= 2 to satisfy invariants (1) and (2).
data: make([]T, initSize),
right: 1,
}
}
// Invariants after each function call and before the first call:
// (1) The next element pushed left will be placed at data[left]
// (2) The next element pushed right will be placed at data[right]
// (3) There are [size] elements in the deque.
type unboundedSliceDeque[T any] struct {
size, left, right int
data []T
}
func (b *unboundedSliceDeque[T]) PushRight(elt T) bool {
// Invariant (2) says it's safe to place the element without resizing.
b.data[b.right] = elt
b.size++
b.right++
b.right %= len(b.data)
b.resize()
return true
}
func (b *unboundedSliceDeque[T]) PushLeft(elt T) bool {
// Invariant (1) says it's safe to place the element without resizing.
b.data[b.left] = elt
b.size++
b.left--
if b.left < 0 {
b.left = len(b.data) - 1 // Wrap around
}
b.resize()
return true
}
func (b *unboundedSliceDeque[T]) PopLeft() (T, bool) {
if b.size == 0 {
return utils.Zero[T](), false
}
idx := b.leftmostEltIdx()
elt := b.data[idx]
// Zero out to prevent memory leak.
b.data[idx] = utils.Zero[T]()
b.size--
b.left++
b.left %= len(b.data)
return elt, true
}
func (b *unboundedSliceDeque[T]) PeekLeft() (T, bool) {
if b.size == 0 {
return utils.Zero[T](), false
}
idx := b.leftmostEltIdx()
return b.data[idx], true
}
func (b *unboundedSliceDeque[T]) PopRight() (T, bool) {
if b.size == 0 {
return utils.Zero[T](), false
}
idx := b.rightmostEltIdx()
elt := b.data[idx]
// Zero out to prevent memory leak.
b.data[idx] = utils.Zero[T]()
b.size--
b.right--
if b.right < 0 {
b.right = len(b.data) - 1 // Wrap around
}
return elt, true
}
func (b *unboundedSliceDeque[T]) PeekRight() (T, bool) {
if b.size == 0 {
return utils.Zero[T](), false
}
idx := b.rightmostEltIdx()
return b.data[idx], true
}
func (b *unboundedSliceDeque[T]) Index(idx int) (T, bool) {
if idx < 0 || idx >= b.size {
return utils.Zero[T](), false
}
leftmostIdx := b.leftmostEltIdx()
idx = (leftmostIdx + idx) % len(b.data)
return b.data[idx], true
}
func (b *unboundedSliceDeque[T]) Len() int {
return b.size
}
func (b *unboundedSliceDeque[T]) List() []T {
if b.size == 0 {
return nil
}
list := make([]T, b.size)
leftmostIdx := b.leftmostEltIdx()
if numCopied := copy(list, b.data[leftmostIdx:]); numCopied < b.size {
// We copied all of the elements from the leftmost element index
// to the end of the underlying slice, but we still haven't copied
// all of the elements, so wrap around and copy the rest.
copy(list[numCopied:], b.data[:b.right])
}
return list
}
func (b *unboundedSliceDeque[T]) leftmostEltIdx() int {
if b.left == len(b.data)-1 { // Wrap around case
return 0
}
return b.left + 1 // Normal case
}
func (b *unboundedSliceDeque[T]) rightmostEltIdx() int {
if b.right == 0 {
return len(b.data) - 1 // Wrap around case
}
return b.right - 1 // Normal case
}
func (b *unboundedSliceDeque[T]) resize() {
if b.size != len(b.data) {
return
}
newData := make([]T, b.size*2)
leftmostIdx := b.leftmostEltIdx()
copy(newData, b.data[leftmostIdx:])
numCopied := len(b.data) - leftmostIdx
copy(newData[numCopied:], b.data[:b.right])
b.data = newData
b.left = len(b.data) - 1
b.right = b.size
}
+672
View File
@@ -0,0 +1,672 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package buffer
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestUnboundedDeque_InitialCapGreaterThanMin(t *testing.T) {
require := require.New(t)
bIntf := NewUnboundedDeque[int](10)
require.IsType(&unboundedSliceDeque[int]{}, bIntf)
b := bIntf.(*unboundedSliceDeque[int])
require.Empty(b.List())
require.Zero(b.Len())
_, ok := b.Index(0)
require.False(ok)
b.PushLeft(1)
require.Equal(1, b.Len())
require.Equal([]int{1}, b.List())
got, ok := b.Index(0)
require.True(ok)
require.Equal(1, got)
_, ok = b.Index(1)
require.False(ok)
got, ok = b.PopLeft()
require.Zero(b.Len())
require.True(ok)
require.Equal(1, got)
_, ok = b.Index(0)
require.False(ok)
b.PushLeft(1)
require.Equal(1, b.Len())
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.PopRight()
require.Zero(b.Len())
require.True(ok)
require.Equal(1, got)
require.Empty(b.List())
_, ok = b.Index(0)
require.False(ok)
b.PushRight(1)
require.Equal(1, b.Len())
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.PopRight()
require.Zero(b.Len())
require.True(ok)
require.Equal(1, got)
require.Empty(b.List())
_, ok = b.Index(0)
require.False(ok)
b.PushRight(1)
require.Equal(1, b.Len())
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.PopLeft()
require.Zero(b.Len())
require.True(ok)
require.Equal(1, got)
require.Empty(b.List())
_, ok = b.Index(0)
require.False(ok)
b.PushLeft(1)
require.Equal(1, b.Len())
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
b.PushLeft(2)
require.Equal(2, b.Len())
require.Equal([]int{2, 1}, b.List())
got, ok = b.PopLeft()
require.Equal(1, b.Len())
require.True(ok)
require.Equal(2, got)
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.PopLeft()
require.Zero(b.Len())
require.True(ok)
require.Equal(1, got)
require.Empty(b.List())
_, ok = b.Index(0)
require.False(ok)
b.PushRight(1)
require.Equal(1, b.Len())
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
b.PushRight(2)
require.Equal(2, b.Len())
require.Equal([]int{1, 2}, b.List())
got, ok = b.PopRight()
require.Equal(1, b.Len())
require.True(ok)
require.Equal(2, got)
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.PopRight()
require.Zero(b.Len())
require.True(ok)
require.Equal(1, got)
require.Empty(b.List())
_, ok = b.Index(0)
require.False(ok)
b.PushLeft(1)
require.Equal(1, b.Len())
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
b.PushLeft(2)
require.Equal(2, b.Len())
require.Equal([]int{2, 1}, b.List())
got, ok = b.PopRight()
require.Equal(1, b.Len())
require.True(ok)
require.Equal(1, got)
require.Equal([]int{2}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(2, got)
got, ok = b.PopLeft()
require.Zero(b.Len())
require.True(ok)
require.Equal(2, got)
require.Empty(b.List())
_, ok = b.Index(0)
require.False(ok)
b.PushRight(1)
require.Equal(1, b.Len())
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
b.PushLeft(2)
require.Equal(2, b.Len())
require.Equal([]int{2, 1}, b.List())
got, ok = b.PopRight()
require.Equal(1, b.Len())
require.True(ok)
require.Equal(1, got)
require.Equal([]int{2}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(2, got)
got, ok = b.PopLeft()
require.Zero(b.Len())
require.True(ok)
require.Equal(2, got)
require.Empty(b.List())
_, ok = b.Index(0)
require.False(ok)
b.PushLeft(1)
require.Equal(1, b.Len())
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
b.PushRight(2)
require.Equal(2, b.Len())
require.Equal([]int{1, 2}, b.List())
got, ok = b.PopLeft()
require.Equal(1, b.Len())
require.True(ok)
require.Equal(1, got)
require.Equal([]int{2}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(2, got)
got, ok = b.PopRight()
require.Zero(b.Len())
require.True(ok)
require.Equal(2, got)
require.Empty(b.List())
_, ok = b.Index(0)
require.False(ok)
}
// Cases we test:
// 1. [left] moves to the left (no wrap around).
// 2. [left] moves to the right (no wrap around).
// 3. [left] wrapping around to the left side.
// 4. [left] wrapping around to the right side.
// 5. Resize.
func TestUnboundedSliceDequePushLeftPopLeft(t *testing.T) {
require := require.New(t)
// Starts empty.
bIntf := NewUnboundedDeque[int](2)
require.IsType(&unboundedSliceDeque[int]{}, bIntf)
b := bIntf.(*unboundedSliceDeque[int])
require.Zero(bIntf.Len())
require.Len(b.data, 2)
require.Zero(b.left)
require.Equal(1, b.right)
require.Empty(b.List())
// slice is [EMPTY]
_, ok := b.PopLeft()
require.False(ok)
_, ok = b.PeekLeft()
require.False(ok)
_, ok = b.PeekRight()
require.False(ok)
b.PushLeft(1) // slice is [1,EMPTY]
require.Equal(1, b.Len())
require.Len(b.data, 2)
require.Equal(1, b.left)
require.Equal(1, b.right)
require.Equal([]int{1}, b.List())
got, ok := b.PeekLeft()
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(1, got)
// This causes a resize
b.PushLeft(2) // slice is [2,1,EMPTY,EMPTY]
require.Equal(2, b.Len())
require.Len(b.data, 4)
require.Equal(3, b.left)
require.Equal(2, b.right)
require.Equal([]int{2, 1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(2, got)
got, ok = b.Index(1)
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekLeft()
require.True(ok)
require.Equal(2, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(1, got)
// Tests left moving left with no wrap around.
b.PushLeft(3) // slice is [2,1,EMPTY,3]
require.Equal(3, b.Len())
require.Len(b.data, 4)
require.Equal(2, b.left)
require.Equal(2, b.right)
require.Equal([]int{3, 2, 1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(3, got)
got, ok = b.Index(1)
require.True(ok)
require.Equal(2, got)
got, ok = b.Index(2)
require.True(ok)
require.Equal(1, got)
_, ok = b.Index(3)
require.False(ok)
got, ok = b.PeekLeft()
require.True(ok)
require.Equal(3, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(1, got)
// Tests left moving right with no wrap around.
got, ok = b.PopLeft() // slice is [2,1,EMPTY,EMPTY]
require.True(ok)
require.Equal(3, got)
require.Equal(2, b.Len())
require.Len(b.data, 4)
require.Equal(3, b.left)
require.Equal(2, b.right)
require.Equal([]int{2, 1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(2, got)
got, ok = b.Index(1)
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekLeft()
require.True(ok)
require.Equal(2, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(1, got)
// Tests left wrapping around to the left side.
got, ok = b.PopLeft() // slice is [EMPTY,1,EMPTY,EMPTY]
require.True(ok)
require.Equal(2, got)
require.Equal(1, b.Len())
require.Len(b.data, 4)
require.Zero(b.left)
require.Equal(2, b.right)
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekLeft()
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(1, got)
// Test left wrapping around to the right side.
b.PushLeft(2) // slice is [2,1,EMPTY,EMPTY]
require.Equal(2, b.Len())
require.Len(b.data, 4)
require.Equal(3, b.left)
require.Equal(2, b.right)
require.Equal([]int{2, 1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(2, got)
got, ok = b.Index(1)
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekLeft()
require.True(ok)
require.Equal(2, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(1, got)
got, ok = b.PopLeft() // slice is [EMPTY,1,EMPTY,EMPTY]
require.True(ok)
require.Equal(2, got)
require.Equal(1, b.Len())
require.Len(b.data, 4)
require.Zero(b.left)
require.Equal(2, b.right)
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.PopLeft() // slice is [EMPTY,EMPTY,EMPTY,EMPTY]
require.True(ok)
require.Equal(1, got)
require.Zero(b.Len())
require.Len(b.data, 4)
require.Equal(1, b.left)
require.Equal(2, b.right)
require.Empty(b.List())
_, ok = b.Index(0)
require.False(ok)
_, ok = b.PopLeft()
require.False(ok)
_, ok = b.PeekLeft()
require.False(ok)
_, ok = b.PeekRight()
require.False(ok)
}
func TestUnboundedSliceDequePushRightPopRight(t *testing.T) {
require := require.New(t)
// Starts empty.
bIntf := NewUnboundedDeque[int](2)
require.IsType(&unboundedSliceDeque[int]{}, bIntf)
b := bIntf.(*unboundedSliceDeque[int])
require.Zero(bIntf.Len())
require.Len(b.data, 2)
require.Zero(b.left)
require.Equal(1, b.right)
require.Empty(b.List())
// slice is [EMPTY]
_, ok := b.PopRight()
require.False(ok)
_, ok = b.PeekLeft()
require.False(ok)
_, ok = b.PeekRight()
require.False(ok)
b.PushRight(1) // slice is [1,EMPTY]
require.Equal(1, b.Len())
require.Len(b.data, 2)
require.Zero(b.left)
require.Zero(b.right)
require.Equal([]int{1}, b.List())
got, ok := b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekLeft()
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(1, got)
// This causes a resize
b.PushRight(2) // slice is [1,2,EMPTY,EMPTY]
require.Equal(2, b.Len())
require.Len(b.data, 4)
require.Equal(3, b.left)
require.Equal(2, b.right)
require.Equal([]int{1, 2}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.Index(1)
require.True(ok)
require.Equal(2, got)
got, ok = b.PeekLeft()
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(2, got)
// Tests right moving right with no wrap around
b.PushRight(3) // slice is [1,2,3,EMPTY]
require.Equal(3, b.Len())
require.Len(b.data, 4)
require.Equal(3, b.left)
require.Equal(3, b.right)
require.Equal([]int{1, 2, 3}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.Index(1)
require.True(ok)
require.Equal(2, got)
got, ok = b.Index(2)
require.True(ok)
require.Equal(3, got)
got, ok = b.PeekLeft()
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(3, got)
// Tests right moving left with no wrap around
got, ok = b.PopRight() // slice is [1,2,EMPTY,EMPTY]
require.True(ok)
require.Equal(3, got)
require.Equal(2, b.Len())
require.Len(b.data, 4)
require.Equal(3, b.left)
require.Equal(2, b.right)
require.Equal([]int{1, 2}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.Index(1)
require.True(ok)
require.Equal(2, got)
_, ok = b.Index(2)
require.False(ok)
got, ok = b.PeekLeft()
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(2, got)
got, ok = b.PopRight() // slice is [1,EMPTY,EMPTY,EMPTY]
require.True(ok)
require.Equal(2, got)
require.Equal(1, b.Len())
require.Len(b.data, 4)
require.Equal(3, b.left)
require.Equal(1, b.right)
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
_, ok = b.Index(1)
require.False(ok)
got, ok = b.PeekLeft()
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(1, got)
got, ok = b.PopRight() // slice is [EMPTY,EMPTY,EMPTY,EMPTY]
require.True(ok)
require.Equal(1, got)
require.Zero(b.Len())
require.Len(b.data, 4)
require.Equal(3, b.left)
require.Zero(b.right)
require.Empty(b.List())
require.Zero(b.Len())
_, ok = b.Index(0)
require.False(ok)
_, ok = b.PeekLeft()
require.False(ok)
_, ok = b.PeekRight()
require.False(ok)
_, ok = b.PopRight()
require.False(ok)
b.PushLeft(1) // slice is [EMPTY,EMPTY,EMPTY,1]
require.Equal(1, b.Len())
require.Len(b.data, 4)
require.Equal(2, b.left)
require.Zero(b.right)
require.Equal([]int{1}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekLeft()
require.True(ok)
require.Equal(1, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(1, got)
// Test right wrapping around to the right
got, ok = b.PopRight() // slice is [EMPTY,EMPTY,EMPTY,EMPTY]
require.True(ok)
require.Equal(1, got)
require.Zero(b.Len())
require.Len(b.data, 4)
require.Equal(2, b.left)
require.Equal(3, b.right)
require.Empty(b.List())
require.Zero(b.Len())
_, ok = b.Index(0)
require.False(ok)
_, ok = b.PeekLeft()
require.False(ok)
_, ok = b.PeekRight()
require.False(ok)
// Tests right wrapping around to the left
b.PushRight(2) // slice is [EMPTY,EMPTY,EMPTY,2]
require.Equal(1, b.Len())
require.Len(b.data, 4)
require.Equal(2, b.left)
require.Zero(b.right)
require.Equal([]int{2}, b.List())
got, ok = b.Index(0)
require.True(ok)
require.Equal(2, got)
got, ok = b.PeekLeft()
require.True(ok)
require.Equal(2, got)
got, ok = b.PeekRight()
require.True(ok)
require.Equal(2, got)
got, ok = b.PopRight() // slice is [EMPTY,EMPTY,EMPTY,EMPTY]
require.True(ok)
require.Equal(2, got)
require.Empty(b.List())
_, ok = b.Index(0)
require.False(ok)
_, ok = b.PeekLeft()
require.False(ok)
_, ok = b.PeekRight()
require.False(ok)
_, ok = b.PopRight()
require.False(ok)
}
func FuzzUnboundedSliceDeque(f *testing.F) {
f.Fuzz(
func(t *testing.T, initSize uint, input []byte) {
require := require.New(t)
b := NewUnboundedDeque[byte](int(initSize))
for i, n := range input {
b.PushRight(n)
gotIndex, ok := b.Index(i)
require.True(ok)
require.Equal(n, gotIndex)
}
list := b.List()
require.Len(list, len(input))
for i, n := range input {
require.Equal(n, list[i])
}
for i := 0; i < len(input); i++ {
_, _ = b.PopLeft()
list = b.List()
if i == len(input)-1 {
require.Empty(list)
_, ok := b.Index(0)
require.False(ok)
} else {
require.Equal(input[i+1:], list)
got, ok := b.Index(0)
require.True(ok)
require.Equal(input[i+1], got)
}
}
},
)
}
+21
View File
@@ -0,0 +1,21 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package iterator
import "github.com/luxfi/utils"
var _ Iterator[any] = Empty[any]{}
// Empty is an iterator with no elements.
type Empty[T any] struct{}
func (Empty[_]) Next() bool {
return false
}
func (Empty[T]) Value() T {
return utils.Zero[T]()
}
func (Empty[_]) Release() {}
+24
View File
@@ -0,0 +1,24 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package iterator
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestEmpty(t *testing.T) {
var (
require = require.New(t)
empty = Empty[*int]{}
)
require.False(empty.Next())
empty.Release()
require.False(empty.Next())
require.Nil(empty.Value())
}
+53
View File
@@ -0,0 +1,53 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package iterator
import "github.com/luxfi/math/set"
var _ Iterator[any] = (*filtered[any])(nil)
type filtered[T any] struct {
it Iterator[T]
filter func(T) bool
}
// Filter returns an iterator that skips the elements in [it] that return true
// from [filter].
func Filter[T any](it Iterator[T], filter func(T) bool) Iterator[T] {
return &filtered[T]{
it: it,
filter: filter,
}
}
// Deduplicate returns an iterator that skips the elements that have already
// been returned from [it].
func Deduplicate[T comparable](it Iterator[T]) Iterator[T] {
seen := set.Of[T]()
return Filter(it, func(e T) bool {
if seen.Contains(e) {
return true
}
seen.Add(e)
return false
})
}
func (i *filtered[_]) Next() bool {
for i.it.Next() {
element := i.it.Value()
if !i.filter(element) {
return true
}
}
return false
}
func (i *filtered[T]) Value() T {
return i.it.Value()
}
func (i *filtered[_]) Release() {
i.it.Release()
}
+65
View File
@@ -0,0 +1,65 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package iterator_test
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/ids"
. "github.com/luxfi/container/iterator"
)
func TestFilter(t *testing.T) {
require := require.New(t)
stakers := []*testStaker{
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(0, 0),
},
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(1, 0),
},
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(2, 0),
},
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(3, 0),
},
}
maskedStakers := map[ids.ID]*testStaker{
stakers[0].TxID: stakers[0],
stakers[2].TxID: stakers[2],
stakers[3].TxID: stakers[3],
}
it := Filter(
FromSlice(stakers[:3]...),
func(staker *testStaker) bool {
_, ok := maskedStakers[staker.TxID]
return ok
},
)
require.True(it.Next())
require.Equal(stakers[1], it.Value())
require.False(it.Next())
it.Release()
require.False(it.Next())
}
func TestDeduplicate(t *testing.T) {
require.Equal(
t,
[]int{0, 1, 2, 3},
ToSlice(Deduplicate(FromSlice(0, 1, 2, 1, 2, 0, 3))),
)
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package iterator
// Iterator defines an interface for iterating over a set.
type Iterator[T any] interface {
// Next attempts to move the iterator to the next element in the set. It
// returns false once there are no more elements to return.
Next() bool
// Value returns the value of the current element. Value should only be called
// after a call to Next which returned true.
Value() T
// Release any resources associated with the iterator. This must be called
// after the iterator is no longer needed.
Release()
}
+79
View File
@@ -0,0 +1,79 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: utils/iterator/iterator.go
//
// Generated by this command:
//
// mockgen -source=utils/iterator/iterator.go -destination=utils/iterator/iteratormock/iterator.go -package=iteratormock -exclude_interfaces= -mock_names=Iterator=Iterator
//
// Package iteratormock is a generated GoMock package.
package iteratormock
import (
reflect "reflect"
gomock "go.uber.org/mock/gomock"
)
// Iterator is a mock of Iterator interface.
type Iterator[T any] struct {
ctrl *gomock.Controller
recorder *IteratorMockRecorder[T]
}
// IteratorMockRecorder is the mock recorder for Iterator.
type IteratorMockRecorder[T any] struct {
mock *Iterator[T]
}
// NewIterator creates a new mock instance.
func NewIterator[T any](ctrl *gomock.Controller) *Iterator[T] {
mock := &Iterator[T]{ctrl: ctrl}
mock.recorder = &IteratorMockRecorder[T]{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Iterator[T]) EXPECT() *IteratorMockRecorder[T] {
return m.recorder
}
// Next mocks base method.
func (m *Iterator[T]) Next() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Next")
ret0, _ := ret[0].(bool)
return ret0
}
// Next indicates an expected call of Next.
func (mr *IteratorMockRecorder[T]) Next() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Next", reflect.TypeOf((*Iterator[T])(nil).Next))
}
// Release mocks base method.
func (m *Iterator[T]) Release() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Release")
}
// Release indicates an expected call of Release.
func (mr *IteratorMockRecorder[T]) Release() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Release", reflect.TypeOf((*Iterator[T])(nil).Release))
}
// Value mocks base method.
func (m *Iterator[T]) Value() T {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Value")
ret0, _ := ret[0].(T)
return ret0
}
// Value indicates an expected call of Value.
func (mr *IteratorMockRecorder[T]) Value() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Value", reflect.TypeOf((*Iterator[T])(nil).Value))
}
+89
View File
@@ -0,0 +1,89 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package iterator
import (
"github.com/google/btree"
"github.com/luxfi/utils/heap"
)
var _ Iterator[any] = (*merged[any])(nil)
type merged[T any] struct {
initialized bool
// heap only contains iterators that have been initialized and are not
// exhausted.
heap heap.Queue[Iterator[T]]
}
// Merge returns an iterator that returns all of the elements of [iterators] in
// order.
func Merge[T any](less btree.LessFunc[T], iterators ...Iterator[T]) Iterator[T] {
// Filter out iterators that are already exhausted.
i := 0
for i < len(iterators) {
it := iterators[i]
if it.Next() {
i++
continue
}
it.Release()
newLength := len(iterators) - 1
iterators[i] = iterators[newLength]
iterators[newLength] = nil
iterators = iterators[:newLength]
}
it := &merged[T]{
heap: heap.QueueOf(
func(a, b Iterator[T]) bool {
return less(a.Value(), b.Value())
},
iterators...,
),
}
return it
}
func (it *merged[_]) Next() bool {
if it.heap.Len() == 0 {
return false
}
if !it.initialized {
// Note that on the first call to Next() (i.e. here) we don't call
// Next() on the current iterator. This is because we already called
// Next() on each iterator in Merge.
it.initialized = true
return true
}
// Update the heap root.
current, _ := it.heap.Peek()
if current.Next() {
// Calling Next() above modifies [current] so we fix the heap.
it.heap.Fix(0)
return true
}
// The old root is exhausted. Remove it from the heap.
current.Release()
it.heap.Pop()
return it.heap.Len() > 0
}
func (it *merged[T]) Value() T {
peek, _ := it.heap.Peek()
return peek.Value()
}
func (it *merged[_]) Release() {
for it.heap.Len() > 0 {
removed, _ := it.heap.Pop()
removed.Release()
}
}
+224
View File
@@ -0,0 +1,224 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package iterator_test
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/ids"
"github.com/luxfi/container/iterator"
)
func TestMerge(t *testing.T) {
type test struct {
name string
iterators []iterator.Iterator[*testStaker]
expected []*testStaker
}
txID := ids.GenerateTestID()
tests := []test{
{
name: "no iterators",
iterators: []iterator.Iterator[*testStaker]{},
expected: []*testStaker{},
},
{
name: "one empty iterator",
iterators: []iterator.Iterator[*testStaker]{iterator.Empty[*testStaker]{}},
expected: []*testStaker{},
},
{
name: "multiple empty iterator",
iterators: []iterator.Iterator[*testStaker]{iterator.Empty[*testStaker]{}, iterator.Empty[*testStaker]{}, iterator.Empty[*testStaker]{}},
expected: []*testStaker{},
},
{
name: "mixed empty iterators",
iterators: []iterator.Iterator[*testStaker]{iterator.Empty[*testStaker]{}, iterator.FromSlice[*testStaker]()},
expected: []*testStaker{},
},
{
name: "single iterator",
iterators: []iterator.Iterator[*testStaker]{
iterator.FromSlice[*testStaker](
&testStaker{
TxID: txID,
NextTime: time.Unix(0, 0),
},
&testStaker{
TxID: txID,
NextTime: time.Unix(1, 0),
},
),
},
expected: []*testStaker{
{
TxID: txID,
NextTime: time.Unix(0, 0),
},
{
TxID: txID,
NextTime: time.Unix(1, 0),
},
},
},
{
name: "multiple iterators",
iterators: []iterator.Iterator[*testStaker]{
iterator.FromSlice[*testStaker](
&testStaker{
TxID: txID,
NextTime: time.Unix(0, 0),
},
&testStaker{
TxID: txID,
NextTime: time.Unix(2, 0),
},
),
iterator.FromSlice[*testStaker](
&testStaker{
TxID: txID,
NextTime: time.Unix(1, 0),
},
&testStaker{
TxID: txID,
NextTime: time.Unix(3, 0),
},
),
},
expected: []*testStaker{
{
TxID: txID,
NextTime: time.Unix(0, 0),
},
{
TxID: txID,
NextTime: time.Unix(1, 0),
},
{
TxID: txID,
NextTime: time.Unix(2, 0),
},
{
TxID: txID,
NextTime: time.Unix(3, 0),
},
},
},
{
name: "multiple iterators different lengths",
iterators: []iterator.Iterator[*testStaker]{
iterator.FromSlice[*testStaker](
&testStaker{
TxID: txID,
NextTime: time.Unix(0, 0),
},
&testStaker{
TxID: txID,
NextTime: time.Unix(2, 0),
},
),
iterator.FromSlice[*testStaker](
&testStaker{
TxID: txID,
NextTime: time.Unix(1, 0),
},
&testStaker{
TxID: txID,
NextTime: time.Unix(3, 0),
},
&testStaker{
TxID: txID,
NextTime: time.Unix(4, 0),
},
&testStaker{
TxID: txID,
NextTime: time.Unix(5, 0),
},
),
},
expected: []*testStaker{
{
TxID: txID,
NextTime: time.Unix(0, 0),
},
{
TxID: txID,
NextTime: time.Unix(1, 0),
},
{
TxID: txID,
NextTime: time.Unix(2, 0),
},
{
TxID: txID,
NextTime: time.Unix(3, 0),
},
{
TxID: txID,
NextTime: time.Unix(4, 0),
},
{
TxID: txID,
NextTime: time.Unix(5, 0),
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
it := iterator.Merge[*state.Staker]((*state.Staker).Less, tt.iterators...)
for _, expected := range tt.expected {
require.True(it.Next())
require.Equal(expected, it.Value())
}
require.False(it.Next())
it.Release()
require.False(it.Next())
})
}
}
func TestMergedEarlyRelease(t *testing.T) {
require := require.New(t)
stakers0 := []*state.Staker{
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(0, 0),
},
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(2, 0),
},
}
stakers1 := []*state.Staker{
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(1, 0),
},
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(3, 0),
},
}
it := iterator.Merge(
(*state.Staker).Less,
iterator.Empty[*state.Staker]{},
iterator.FromSlice(stakers0...),
iterator.Empty[*state.Staker]{},
iterator.FromSlice(stakers1...),
iterator.Empty[*state.Staker]{},
)
require.True(it.Next())
it.Release()
require.False(it.Next())
}
+43
View File
@@ -0,0 +1,43 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package iterator
var _ Iterator[any] = (*slice[any])(nil)
// ToSlice returns a slice that contains all of the elements from [it] in order.
// [it] will be released before returning.
func ToSlice[T any](it Iterator[T]) []T {
defer it.Release()
var elements []T
for it.Next() {
elements = append(elements, it.Value())
}
return elements
}
type slice[T any] struct {
index int
elements []T
}
// FromSlice returns an iterator that contains [elements] in order. Doesn't sort
// by anything.
func FromSlice[T any](elements ...T) Iterator[T] {
return &slice[T]{
index: -1,
elements: elements,
}
}
func (i *slice[_]) Next() bool {
i.index++
return i.index < len(i.elements)
}
func (i *slice[T]) Value() T {
return i.elements[i.index]
}
func (*slice[_]) Release() {}
+63
View File
@@ -0,0 +1,63 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package iterator
import (
"sync"
"github.com/google/btree"
)
var _ Iterator[any] = (*tree[any])(nil)
type tree[T any] struct {
current T
next chan T
releaseOnce sync.Once
release chan struct{}
wg sync.WaitGroup
}
// FromTree returns a new iterator of the stakers in [tree] in ascending order.
// Note that it isn't safe to modify [tree] while iterating over it.
func FromTree[T any](btree *btree.BTreeG[T]) Iterator[T] {
if btree == nil {
return Empty[T]{}
}
it := &tree[T]{
next: make(chan T),
release: make(chan struct{}),
}
it.wg.Add(1)
go func() {
defer it.wg.Done()
btree.Ascend(func(i T) bool {
select {
case it.next <- i:
return true
case <-it.release:
return false
}
})
close(it.next)
}()
return it
}
func (i *tree[_]) Next() bool {
next, ok := <-i.next
i.current = next
return ok
}
func (i *tree[T]) Value() T {
return i.current
}
func (i *tree[_]) Release() {
i.releaseOnce.Do(func() {
close(i.release)
})
i.wg.Wait()
}
+91
View File
@@ -0,0 +1,91 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package iterator_test
import (
"testing"
"time"
"github.com/google/btree"
"github.com/stretchr/testify/require"
"github.com/luxfi/ids"
"github.com/luxfi/container/iterator"
)
var defaultTreeDegree = 2
type testStaker struct {
TxID ids.ID
NextTime time.Time
}
func (s *testStaker) Less(other *testStaker) bool {
return s.NextTime.Before(other.NextTime)
}
func TestTree(t *testing.T) {
require := require.New(t)
stakers := []*testStaker{
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(0, 0),
},
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(1, 0),
},
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(2, 0),
},
}
tree := btree.NewG(defaultTreeDegree, (*testStaker).Less)
for _, staker := range stakers {
require.Nil(tree.ReplaceOrInsert(staker))
}
it := iterator.FromTree(tree)
for _, staker := range stakers {
require.True(it.Next())
require.Equal(staker, it.Value())
}
require.False(it.Next())
it.Release()
}
func TestTreeNil(t *testing.T) {
it := iterator.FromTree[*testStaker](nil)
require.False(t, it.Next())
it.Release()
}
func TestTreeEarlyRelease(t *testing.T) {
require := require.New(t)
stakers := []*testStaker{
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(0, 0),
},
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(1, 0),
},
{
TxID: ids.GenerateTestID(),
NextTime: time.Unix(2, 0),
},
}
tree := btree.NewG(defaultTreeDegree, (*testStaker).Less)
for _, staker := range stakers {
require.Nil(tree.ReplaceOrInsert(staker))
}
it := iterator.FromTree(tree)
require.True(it.Next())
it.Release()
require.False(it.Next())
}
+86
View File
@@ -0,0 +1,86 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"math"
"sync"
"time"
"gonum.org/v1/gonum/mathext/prng"
)
var globalRNG = newRNG()
func newRNG() *rng {
// We don't use a cryptographically secure source of randomness here, as
// there's no need to ensure a truly random sampling.
source := prng.NewMT19937()
source.Seed(uint64(time.Now().UnixNano()))
return &rng{rng: source}
}
type rng struct {
lock sync.Mutex
rng Source
}
type Source interface {
// Uint64 returns a random number in [0, MaxUint64] and advances the
// generator's state.
Uint64() uint64
}
// Uint64Inclusive returns a pseudo-random number in [0,n].
//
// Invariant: The result of this function is stored in chain state, so any
// modifications are considered breaking.
func (r *rng) Uint64Inclusive(n uint64) uint64 {
switch {
// n+1 is power of two, so we can just mask
//
// Note: This does work for MaxUint64 as overflow is explicitly part of the
// compiler specification: https://go.dev/ref/spec#Integer_overflow
case n&(n+1) == 0:
return r.uint64() & n
// n is greater than MaxUint64/2 so we need to just iterate until we get a
// number in the requested range.
case n > math.MaxInt64:
v := r.uint64()
for v > n {
v = r.uint64()
}
return v
// n is less than MaxUint64/2 so we generate a number in the range
// [0, k*(n+1)) where k is the largest integer such that k*(n+1) is less
// than or equal to MaxUint64/2. We can't easily find k such that k*(n+1) is
// less than or equal to MaxUint64 because the calculation would overflow.
//
// ref: https://github.com/golang/go/blob/ce10e9d84574112b224eae88dc4e0f43710808de/src/math/rand/rand.go#L127-L132
default:
maximum := (1 << 63) - 1 - (1<<63)%(n+1)
v := r.uint63()
for v > maximum {
v = r.uint63()
}
return v % (n + 1)
}
}
// uint63 returns a random number in [0, MaxInt64]
func (r *rng) uint63() uint64 {
return r.uint64() & math.MaxInt64
}
// uint64 returns a random number in [0, MaxUint64]
func (r *rng) uint64() uint64 {
// Note: We must grab a write lock here because rng.Uint64 internally
// modifies state.
r.lock.Lock()
n := r.rng.Uint64()
r.lock.Unlock()
return n
}
+228
View File
@@ -0,0 +1,228 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"math"
"math/rand"
"strconv"
"testing"
"github.com/stretchr/testify/require"
"github.com/thepudds/fzgen/fuzzer"
"gonum.org/v1/gonum/mathext/prng"
)
type testSource struct {
onInvalid func()
nums []uint64
}
func (s *testSource) Seed(uint64) {
s.onInvalid()
}
func (s *testSource) Uint64() uint64 {
if len(s.nums) == 0 {
s.onInvalid()
}
num := s.nums[0]
s.nums = s.nums[1:]
return num
}
type testSTDSource struct {
onInvalid func()
nums []uint64
}
func (s *testSTDSource) Seed(int64) {
s.onInvalid()
}
func (s *testSTDSource) Int63() int64 {
return int64(s.Uint64() & (1<<63 - 1))
}
func (s *testSTDSource) Uint64() uint64 {
if len(s.nums) == 0 {
s.onInvalid()
}
num := s.nums[0]
s.nums = s.nums[1:]
return num
}
func TestRNG(t *testing.T) {
tests := []struct {
maximum uint64
nums []uint64
expected uint64
}{
{
maximum: math.MaxUint64,
nums: []uint64{
0x01,
},
expected: 0x01,
},
{
maximum: math.MaxUint64,
nums: []uint64{
0x0102030405060708,
},
expected: 0x0102030405060708,
},
{
maximum: math.MaxUint64,
nums: []uint64{
0xF102030405060708,
},
expected: 0xF102030405060708,
},
{
maximum: math.MaxInt64,
nums: []uint64{
0x01,
},
expected: 0x01,
},
{
maximum: math.MaxInt64,
nums: []uint64{
0x0102030405060708,
},
expected: 0x0102030405060708,
},
{
maximum: math.MaxInt64,
nums: []uint64{
0x8102030405060708,
},
expected: 0x0102030405060708,
},
{
maximum: 15,
nums: []uint64{
0x810203040506071a,
},
expected: 0x0a,
},
{
maximum: math.MaxInt64 + 1,
nums: []uint64{
math.MaxInt64 + 1,
},
expected: math.MaxInt64 + 1,
},
{
maximum: math.MaxInt64 + 1,
nums: []uint64{
math.MaxInt64 + 2,
0,
},
expected: 0,
},
{
maximum: math.MaxInt64 + 1,
nums: []uint64{
math.MaxInt64 + 2,
0x0102030405060708,
},
expected: 0x0102030405060708,
},
{
maximum: 2,
nums: []uint64{
math.MaxInt64 - 2,
},
expected: 0x02,
},
{
maximum: 2,
nums: []uint64{
math.MaxInt64 - 1,
0x01,
},
expected: 0x01,
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
require := require.New(t)
source := &testSource{
onInvalid: t.FailNow,
nums: test.nums,
}
r := &rng{rng: source}
val := r.Uint64Inclusive(test.maximum)
require.Equal(test.expected, val)
require.Empty(source.nums)
if test.maximum >= math.MaxInt64 {
return
}
stdSource := &testSTDSource{
onInvalid: t.FailNow,
nums: test.nums,
}
mathRNG := rand.New(stdSource) //#nosec G404
stdVal := mathRNG.Int63n(int64(test.maximum + 1))
require.Equal(test.expected, uint64(stdVal))
require.Empty(source.nums)
})
}
}
func FuzzRNG(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
require := require.New(t)
var (
maximum uint64
sourceNums []uint64
)
fz := fuzzer.NewFuzzer(data)
fz.Fill(&maximum, &sourceNums)
if maximum >= math.MaxInt64 {
t.SkipNow()
}
source := &testSource{
onInvalid: func() { /* Invalid input - continue test */ },
nums: sourceNums,
}
r := &rng{rng: source}
val := r.Uint64Inclusive(maximum)
stdSource := &testSTDSource{
onInvalid: func() { /* Invalid input - continue test */ },
nums: sourceNums,
}
mathRNG := rand.New(stdSource) //#nosec G404
stdVal := mathRNG.Int63n(int64(maximum + 1))
require.Equal(val, uint64(stdVal))
require.Len(stdSource.nums, len(source.nums))
})
}
func BenchmarkSeed32(b *testing.B) {
source := prng.NewMT19937()
b.ResetTimer()
for i := 0; i < b.N; i++ {
source.Seed(0)
}
}
func BenchmarkSeed64(b *testing.B) {
source := prng.NewMT19937_64()
b.ResetTimer()
for i := 0; i < b.N; i++ {
source.Seed(0)
}
}
+32
View File
@@ -0,0 +1,32 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
// Uniform samples values without replacement in the provided range
type Uniform interface {
Initialize(sampleRange uint64)
// Sample returns length numbers in the range [0,sampleRange). If there
// aren't enough numbers in the range, false is returned. If length is
// negative the implementation may panic.
Sample(length int) ([]uint64, bool)
Next() (uint64, bool)
Reset()
}
// NewUniform returns a new sampler
func NewUniform() Uniform {
return &uniformReplacer{
rng: globalRNG,
}
}
// NewDeterministicUniform returns a new sampler
func NewDeterministicUniform(source Source) Uniform {
return &uniformReplacer{
rng: &rng{
rng: source,
},
}
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"fmt"
"testing"
)
func BenchmarkUniform(b *testing.B) {
sizes := []uint64{
30,
35,
500,
10000,
100000,
}
for _, size := range sizes {
b.Run(fmt.Sprintf("%d elements uniformly", size), func(b *testing.B) {
s := NewUniform()
s.Initialize(size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = s.Sample(30)
}
})
}
}
+72
View File
@@ -0,0 +1,72 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"math"
"time"
"github.com/luxfi/timer/mockable"
)
var _ Uniform = (*uniformBest)(nil)
// Sampling is performed by using another implementation of the Uniform
// interface.
//
// Initialization attempts to find the best sampling algorithm given the dataset
// by performing a benchmark of the provided implementations.
type uniformBest struct {
Uniform
samplers []Uniform
maxSampleSize int
benchmarkIterations int
clock mockable.Clock
}
// NewBestUniform returns a new sampler
func NewBestUniform(expectedSampleSize int) Uniform {
return &uniformBest{
samplers: []Uniform{
&uniformReplacer{
rng: globalRNG,
},
&uniformResample{
rng: globalRNG,
},
},
maxSampleSize: expectedSampleSize,
benchmarkIterations: 100,
}
}
func (s *uniformBest) Initialize(length uint64) {
s.Uniform = nil
bestDuration := time.Duration(math.MaxInt64)
sampleSize := s.maxSampleSize
if length < uint64(sampleSize) {
sampleSize = int(length)
}
samplerLoop:
for _, sampler := range s.samplers {
sampler.Initialize(length)
start := s.clock.Time()
for i := 0; i < s.benchmarkIterations; i++ {
if _, ok := sampler.Sample(sampleSize); !ok {
continue samplerLoop
}
}
end := s.clock.Time()
duration := end.Sub(start)
if duration < bestDuration {
bestDuration = duration
s.Uniform = sampler
}
}
s.Uniform.Reset()
}
+69
View File
@@ -0,0 +1,69 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
type defaultMap map[uint64]uint64
func (m defaultMap) get(key uint64, defaultVal uint64) uint64 {
if val, ok := m[key]; ok {
return val
}
return defaultVal
}
// uniformReplacer allows for sampling over a uniform distribution without
// replacement.
//
// Sampling is performed by lazily performing an array shuffle of the array
// [0, 1, ..., length - 1]. By performing the first count swaps of this shuffle,
// we can create an array of length count with elements sampled with uniform
// probability.
//
// Initialization takes O(1) time.
//
// Sampling is performed in O(count) time and O(count) space.
type uniformReplacer struct {
rng *rng
length uint64
drawn defaultMap
drawsCount uint64
}
func (s *uniformReplacer) Initialize(length uint64) {
s.length = length
s.drawn = make(defaultMap)
s.drawsCount = 0
}
func (s *uniformReplacer) Sample(count int) ([]uint64, bool) {
s.Reset()
results := make([]uint64, count)
for i := 0; i < count; i++ {
ret, hasNext := s.Next()
if !hasNext {
return nil, false
}
results[i] = ret
}
return results, true
}
func (s *uniformReplacer) Reset() {
clear(s.drawn)
s.drawsCount = 0
}
func (s *uniformReplacer) Next() (uint64, bool) {
if s.drawsCount >= s.length {
return 0, false
}
draw := s.rng.Uint64Inclusive(s.length-1-s.drawsCount) + s.drawsCount
ret := s.drawn.get(draw, draw)
s.drawn[draw] = s.drawn.get(s.drawsCount, s.drawsCount)
s.drawsCount++
return ret, true
}
+58
View File
@@ -0,0 +1,58 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
// uniformResample allows for sampling over a uniform distribution without
// replacement.
//
// Sampling is performed by sampling with replacement and resampling if a
// duplicate is sampled.
//
// Initialization takes O(1) time.
//
// Sampling is performed in O(count) time and O(count) space.
type uniformResample struct {
rng *rng
length uint64
drawn map[uint64]struct{}
}
func (s *uniformResample) Initialize(length uint64) {
s.length = length
s.drawn = make(map[uint64]struct{})
}
func (s *uniformResample) Sample(count int) ([]uint64, bool) {
s.Reset()
results := make([]uint64, count)
for i := 0; i < count; i++ {
ret, hasNext := s.Next()
if !hasNext {
return nil, false
}
results[i] = ret
}
return results, true
}
func (s *uniformResample) Reset() {
clear(s.drawn)
}
func (s *uniformResample) Next() (uint64, bool) {
i := uint64(len(s.drawn))
if i >= s.length {
return 0, false
}
for {
draw := s.rng.Uint64Inclusive(s.length - 1)
if _, ok := s.drawn[draw]; ok {
continue
}
s.drawn[draw] = struct{}{}
return draw, true
}
}
+100
View File
@@ -0,0 +1,100 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"math"
"slices"
"testing"
"github.com/stretchr/testify/require"
)
func TestUniformInitializeMaxUint64(t *testing.T) {
s := NewUniform()
s.Initialize(math.MaxUint64)
for {
val, hasNext := s.Next()
require.True(t, hasNext)
if val > math.MaxInt64 {
break
}
}
}
func TestUniformOutOfRange(t *testing.T) {
s := NewUniform()
s.Initialize(0)
_, ok := s.Sample(1)
require.False(t, ok)
}
func TestUniformEmpty(t *testing.T) {
require := require.New(t)
s := NewUniform()
s.Initialize(1)
val, ok := s.Sample(0)
require.True(ok)
require.Empty(val)
}
func TestUniformSingleton(t *testing.T) {
require := require.New(t)
s := NewUniform()
s.Initialize(1)
val, ok := s.Sample(1)
require.True(ok)
require.Equal([]uint64{0}, val)
}
func TestUniformDistribution(t *testing.T) {
require := require.New(t)
s := NewUniform()
s.Initialize(3)
val, ok := s.Sample(3)
require.True(ok)
slices.Sort(val)
require.Equal([]uint64{0, 1, 2}, val)
}
func TestUniformOverSample(t *testing.T) {
s := NewUniform()
s.Initialize(3)
_, ok := s.Sample(4)
require.False(t, ok)
}
func TestUniformLazilySample(t *testing.T) {
require := require.New(t)
s := NewUniform()
s.Initialize(3)
for j := 0; j < 2; j++ {
sampled := map[uint64]bool{}
for i := 0; i < 3; i++ {
val, hasNext := s.Next()
require.True(hasNext)
require.False(sampled[val])
sampled[val] = true
}
_, hasNext := s.Next()
require.False(hasNext)
s.Reset()
}
}
+15
View File
@@ -0,0 +1,15 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
// Weighted defines how to sample a specified valued based on a provided
// weighted distribution
type Weighted interface {
Initialize(weights []uint64) error
Sample(sampleValue uint64) (int, bool)
}
func NewWeighted() Weighted {
return &weightedHeap{}
}
+125
View File
@@ -0,0 +1,125 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"cmp"
"github.com/luxfi/math"
"github.com/luxfi/utils"
)
var (
_ Weighted = (*weightedArray)(nil)
_ utils.Sortable[weightedArrayElement] = weightedArrayElement{}
)
type weightedArrayElement struct {
cumulativeWeight uint64
index int
}
// Note that this sorts in order of decreasing weight.
func (e weightedArrayElement) Compare(other weightedArrayElement) int {
return cmp.Compare(other.cumulativeWeight, e.cumulativeWeight)
}
// Sampling is performed by executing a modified binary search over the provided
// elements. Rather than cutting the remaining dataset in half, the algorithm
// attempt to just in to where it think the value will be assuming a linear
// distribution of the element weights.
//
// Initialization takes O(n * log(n)) time, where n is the number of elements
// that can be sampled.
// Sampling can take up to O(n) time. If the distribution is linearly
// distributed, then the runtime is constant.
type weightedArray struct {
arr []weightedArrayElement
}
func (s *weightedArray) Initialize(weights []uint64) error {
numWeights := len(weights)
if numWeights <= cap(s.arr) {
s.arr = s.arr[:numWeights]
} else {
s.arr = make([]weightedArrayElement, numWeights)
}
for i, weight := range weights {
s.arr[i] = weightedArrayElement{
cumulativeWeight: weight,
index: i,
}
}
// Optimize so that the array is closer to the uniform distribution
utils.Sort(s.arr)
maxIndex := len(s.arr) - 1
oneIfOdd := 1 & maxIndex
oneIfEven := 1 - oneIfOdd
end := maxIndex - oneIfEven
for i := 1; i < end; i += 2 {
s.arr[i], s.arr[end] = s.arr[end], s.arr[i]
end -= 2
}
cumulativeWeight := uint64(0)
for i := 0; i < len(s.arr); i++ {
newWeight, err := math.Add64(
cumulativeWeight,
s.arr[i].cumulativeWeight,
)
if err != nil {
return err
}
cumulativeWeight = newWeight
s.arr[i].cumulativeWeight = cumulativeWeight
}
return nil
}
func (s *weightedArray) Sample(value uint64) (int, bool) {
if len(s.arr) == 0 || s.arr[len(s.arr)-1].cumulativeWeight <= value {
return 0, false
}
minIndex := 0
maxIndex := len(s.arr) - 1
maxCumulativeWeight := float64(s.arr[len(s.arr)-1].cumulativeWeight)
index := int((float64(value) * float64(maxIndex+1)) / maxCumulativeWeight)
for {
previousWeight := uint64(0)
if index > 0 {
previousWeight = s.arr[index-1].cumulativeWeight
}
currentElem := s.arr[index]
currentWeight := currentElem.cumulativeWeight
if previousWeight <= value && value < currentWeight {
return currentElem.index, true
}
if value < previousWeight {
// go to the left
maxIndex = index - 1
} else {
// go to the right
minIndex = index + 1
}
minWeight := uint64(0)
if minIndex > 0 {
minWeight = s.arr[minIndex-1].cumulativeWeight
}
maxWeight := s.arr[maxIndex].cumulativeWeight
valueRange := maxWeight - minWeight
adjustedLookupValue := value - minWeight
indexRange := maxIndex - minIndex + 1
lookupMass := float64(adjustedLookupValue) * float64(indexRange)
index = int(lookupMass/float64(valueRange)) + minIndex
}
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestWeightedArrayElementCompare(t *testing.T) {
tests := []struct {
a weightedArrayElement
b weightedArrayElement
expected int
}{
{
a: weightedArrayElement{},
b: weightedArrayElement{},
expected: 0,
},
{
a: weightedArrayElement{
cumulativeWeight: 1,
},
b: weightedArrayElement{
cumulativeWeight: 2,
},
expected: 1,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%d_%d_%d", test.a.cumulativeWeight, test.b.cumulativeWeight, test.expected), func(t *testing.T) {
require := require.New(t)
require.Equal(test.expected, test.a.Compare(test.b))
require.Equal(-test.expected, test.b.Compare(test.a))
})
}
}
+148
View File
@@ -0,0 +1,148 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"fmt"
"math"
"testing"
safemath "github.com/luxfi/math"
)
func BenchmarkWeightedHeapSampling(b *testing.B) {
sampler := &weightedHeap{}
pows := []float64{
0,
1,
2,
3,
}
sizes := []int{
10,
500,
1000,
50000,
100000,
}
for _, pow := range pows {
for _, size := range sizes {
if WeightedPowBenchmarkSampler(b, sampler, pow, size) {
b.Run(fmt.Sprintf("%d elements at x^%.1f", size, pow), func(b *testing.B) {
WeightedPowBenchmarkSampler(b, sampler, pow, size)
})
}
}
}
for _, size := range sizes {
if WeightedSingletonBenchmarkSampler(b, sampler, size) {
b.Run(fmt.Sprintf(" %d singleton elements", size), func(b *testing.B) {
WeightedSingletonBenchmarkSampler(b, sampler, size)
})
}
}
}
func BenchmarkWeightedHeapInitializer(b *testing.B) {
sampler := &weightedHeap{}
pows := []float64{
0,
1,
2,
3,
}
sizes := []int{
10,
500,
1000,
50000,
100000,
}
for _, pow := range pows {
for _, size := range sizes {
if WeightedPowBenchmarkSampler(b, sampler, pow, size) {
b.Run(fmt.Sprintf("%d elements at x^%.1f", size, pow), func(b *testing.B) {
_, weights, _ := CalcWeightedPoW(pow, size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = sampler.Initialize(weights)
}
})
}
}
}
for _, size := range sizes {
if WeightedSingletonBenchmarkSampler(b, sampler, size) {
b.Run(fmt.Sprintf("%d singleton elements", size), func(b *testing.B) {
weights := make([]uint64, size)
weights[0] = math.MaxUint64 - uint64(size-1)
for i := 1; i < len(weights); i++ {
weights[i] = 1
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = sampler.Initialize(weights)
}
})
}
}
}
func CalcWeightedPoW(exponent float64, size int) (uint64, []uint64, error) {
weights := make([]uint64, size)
totalWeight := uint64(0)
for i := range weights {
weight := uint64(math.Pow(float64(i+1), exponent))
weights[i] = weight
newWeight, err := safemath.Add(totalWeight, weight)
if err != nil {
return 0, nil, err
}
totalWeight = newWeight
}
return totalWeight, weights, nil
}
func WeightedPowBenchmarkSampler(
b *testing.B,
s Weighted,
exponent float64,
size int,
) bool {
totalWeight, weights, err := CalcWeightedPoW(exponent, size)
if err != nil {
return false
}
if err := s.Initialize(weights); err != nil {
return false
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = s.Sample(globalRNG.Uint64Inclusive(totalWeight - 1))
}
return true
}
func WeightedSingletonBenchmarkSampler(b *testing.B, s Weighted, size int) bool {
weights := make([]uint64, size)
weights[0] = math.MaxUint64 - uint64(size-1)
for i := 1; i < len(weights); i++ {
weights[i] = 1
}
err := s.Initialize(weights)
if err != nil {
return false
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = s.Sample(globalRNG.Uint64Inclusive(math.MaxUint64 - 1))
}
return true
}
+79
View File
@@ -0,0 +1,79 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"errors"
"math"
"time"
"github.com/luxfi/timer/mockable"
safemath "github.com/luxfi/math"
)
var (
errNoValidWeightedSamplers = errors.New("no valid weighted samplers found")
_ Weighted = (*weightedBest)(nil)
)
// Sampling is performed by using another implementation of the Weighted
// interface.
//
// Initialization attempts to find the best sampling algorithm given the dataset
// by performing a benchmark of the provided implementations.
type weightedBest struct {
Weighted
samplers []Weighted
benchmarkIterations int
clock mockable.Clock
}
func (s *weightedBest) Initialize(weights []uint64) error {
totalWeight := uint64(0)
for _, weight := range weights {
newWeight, err := safemath.Add64(totalWeight, weight)
if err != nil {
return err
}
totalWeight = newWeight
}
samples := []uint64(nil)
if totalWeight > 0 {
samples = make([]uint64, s.benchmarkIterations)
for i := range samples {
samples[i] = globalRNG.Uint64Inclusive(totalWeight - 1)
}
}
s.Weighted = nil
bestDuration := time.Duration(math.MaxInt64)
samplerLoop:
for _, sampler := range s.samplers {
if err := sampler.Initialize(weights); err != nil {
continue
}
start := s.clock.Time()
for _, sample := range samples {
if _, ok := sampler.Sample(sample); !ok {
continue samplerLoop
}
}
end := s.clock.Time()
duration := end.Sub(start)
if duration < bestDuration {
bestDuration = duration
s.Weighted = sampler
}
}
if s.Weighted == nil {
return errNoValidWeightedSamplers
}
return nil
}
+107
View File
@@ -0,0 +1,107 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"cmp"
"github.com/luxfi/math"
"github.com/luxfi/utils"
)
var (
_ Weighted = (*weightedHeap)(nil)
_ utils.Sortable[weightedHeapElement] = weightedHeapElement{}
)
type weightedHeapElement struct {
weight uint64
cumulativeWeight uint64
index int
}
// Compare the elements. Weight is in decreasing order. Index is in increasing
// order.
func (e weightedHeapElement) Compare(other weightedHeapElement) int {
// By accounting for the initial index of the weights, this results in a
// stable sort. We do this rather than using `sort.Stable` because of the
// reported change in performance of the sort used.
if weightCmp := cmp.Compare(other.weight, e.weight); weightCmp != 0 {
return weightCmp
}
return cmp.Compare(e.index, other.index)
}
// Sampling is performed by executing a search over a tree of elements in the
// order of their probabilistic occurrence.
//
// Initialization takes O(n * log(n)) time, where n is the number of elements
// that can be sampled.
// Sampling can take up to O(log(n)) time. As the distribution becomes more
// biased, sampling will become faster in expectation.
type weightedHeap struct {
heap []weightedHeapElement
}
func (s *weightedHeap) Initialize(weights []uint64) error {
numWeights := len(weights)
if numWeights <= cap(s.heap) {
s.heap = s.heap[:numWeights]
} else {
s.heap = make([]weightedHeapElement, numWeights)
}
for i, weight := range weights {
s.heap[i] = weightedHeapElement{
weight: weight,
cumulativeWeight: weight,
index: i,
}
}
// Optimize so that the most probable values are at the top of the heap
utils.Sort(s.heap)
// Initialize the heap
for i := len(s.heap) - 1; i > 0; i-- {
// Explicitly performing a shift here allows the compiler to avoid
// checking for negative numbers, which saves a couple cycles
parentIndex := (i - 1) >> 1
newWeight, err := math.Add64(
s.heap[parentIndex].cumulativeWeight,
s.heap[i].cumulativeWeight,
)
if err != nil {
return err
}
s.heap[parentIndex].cumulativeWeight = newWeight
}
return nil
}
func (s *weightedHeap) Sample(value uint64) (int, bool) {
if len(s.heap) == 0 || s.heap[0].cumulativeWeight <= value {
return 0, false
}
index := 0
for {
currentElement := s.heap[index]
currentWeight := currentElement.weight
if value < currentWeight {
return currentElement.index, true
}
value -= currentWeight
// We shouldn't return the root, so check the left child
index = index*2 + 1
if leftWeight := s.heap[index].cumulativeWeight; leftWeight <= value {
// If the weight is greater than the left weight, you should move to
// the right child
value -= leftWeight
index++
}
}
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestWeightedHeapInitialize(t *testing.T) {
require := require.New(t)
h := weightedHeap{}
require.NoError(h.Initialize([]uint64{2, 2, 1, 3, 3, 1, 3}))
expectedOrdering := []int{3, 4, 6, 0, 1, 2, 5}
for i, elem := range h.heap {
expected := expectedOrdering[i]
require.Equal(expected, elem.index)
}
}
func TestWeightedHeapElementCompare(t *testing.T) {
type test struct {
name string
elt1 weightedHeapElement
elt2 weightedHeapElement
expected int
}
tests := []test{
{
name: "all same",
elt1: weightedHeapElement{},
elt2: weightedHeapElement{},
expected: 0,
},
{
name: "lower weight",
elt1: weightedHeapElement{},
elt2: weightedHeapElement{
weight: 1,
},
expected: 1,
},
{
name: "higher index",
elt1: weightedHeapElement{
index: 1,
},
elt2: weightedHeapElement{},
expected: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require := require.New(t)
require.Equal(tt.expected, tt.elt1.Compare(tt.elt2))
require.Equal(-tt.expected, tt.elt2.Compare(tt.elt1))
})
}
}
+83
View File
@@ -0,0 +1,83 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"cmp"
"github.com/luxfi/math"
"github.com/luxfi/utils"
)
var (
_ Weighted = (*weightedLinear)(nil)
_ utils.Sortable[weightedLinearElement] = weightedLinearElement{}
)
type weightedLinearElement struct {
cumulativeWeight uint64
index int
}
// Note that this sorts in order of decreasing cumulative weight.
func (e weightedLinearElement) Compare(other weightedLinearElement) int {
return cmp.Compare(other.cumulativeWeight, e.cumulativeWeight)
}
// Sampling is performed by executing a linear search over the provided elements
// in the order of their probabilistic occurrence.
//
// Initialization takes O(n * log(n)) time, where n is the number of elements
// that can be sampled.
// Sampling can take up to O(n) time. As the distribution becomes more biased,
// sampling will become faster in expectation.
type weightedLinear struct {
arr []weightedLinearElement
}
func (s *weightedLinear) Initialize(weights []uint64) error {
numWeights := len(weights)
if numWeights <= cap(s.arr) {
s.arr = s.arr[:numWeights]
} else {
s.arr = make([]weightedLinearElement, numWeights)
}
for i, weight := range weights {
s.arr[i] = weightedLinearElement{
cumulativeWeight: weight,
index: i,
}
}
// Optimize so that the most probable values are at the front of the array
utils.Sort(s.arr)
for i := 1; i < len(s.arr); i++ {
newWeight, err := math.Add64(
s.arr[i-1].cumulativeWeight,
s.arr[i].cumulativeWeight,
)
if err != nil {
return err
}
s.arr[i].cumulativeWeight = newWeight
}
return nil
}
func (s *weightedLinear) Sample(value uint64) (int, bool) {
if len(s.arr) == 0 || s.arr[len(s.arr)-1].cumulativeWeight <= value {
return 0, false
}
index := 0
for {
if elem := s.arr[index]; value < elem.cumulativeWeight {
return elem.index, true
}
index++
}
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func TestWeightedLinearElementCompare(t *testing.T) {
tests := []struct {
a weightedLinearElement
b weightedLinearElement
expected int
}{
{
a: weightedLinearElement{},
b: weightedLinearElement{},
expected: 0,
},
{
a: weightedLinearElement{
cumulativeWeight: 1,
},
b: weightedLinearElement{
cumulativeWeight: 2,
},
expected: 1,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%d_%d_%d", test.a.cumulativeWeight, test.b.cumulativeWeight, test.expected), func(t *testing.T) {
require := require.New(t)
require.Equal(test.expected, test.a.Compare(test.b))
require.Equal(-test.expected, test.b.Compare(test.a))
})
}
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"math"
"testing"
"github.com/stretchr/testify/require"
safemath "github.com/luxfi/math"
)
func TestWeightedInitializeOverflow(t *testing.T) {
s := &weightedHeap{}
err := s.Initialize([]uint64{1, math.MaxUint64})
require.ErrorIs(t, err, safemath.ErrOverflow)
}
func TestWeightedOutOfRange(t *testing.T) {
require := require.New(t)
s := &weightedHeap{}
require.NoError(s.Initialize([]uint64{1}))
_, ok := s.Sample(1)
require.False(ok)
}
func TestWeightedSingleton(t *testing.T) {
require := require.New(t)
s := &weightedHeap{}
require.NoError(s.Initialize([]uint64{1}))
index, ok := s.Sample(0)
require.True(ok)
require.Zero(index)
}
func TestWeightedWithZero(t *testing.T) {
require := require.New(t)
s := &weightedHeap{}
require.NoError(s.Initialize([]uint64{0, 1}))
index, ok := s.Sample(0)
require.True(ok)
require.Equal(1, index)
}
func TestWeightedDistribution(t *testing.T) {
require := require.New(t)
s := &weightedHeap{}
require.NoError(s.Initialize([]uint64{1, 1, 2, 3, 4}))
counts := make([]int, 5)
for i := uint64(0); i < 11; i++ {
index, ok := s.Sample(i)
require.True(ok)
counts[index]++
}
require.Equal([]int{1, 1, 2, 3, 4}, counts)
}
+69
View File
@@ -0,0 +1,69 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"errors"
"math"
safemath "github.com/luxfi/math"
)
var (
errWeightsTooLarge = errors.New("total weight is too large")
_ Weighted = (*weightedUniform)(nil)
)
// Sampling is performed by indexing into the array to find the correct index.
//
// Initialization takes O(Sum(weights)) time. This results in an exponential
// initialization time. Therefore, the time to execute this operation can be
// extremely long. Initialization takes O(Sum(weights)) space, causing this
// algorithm to be unable to handle large inputs.
//
// Sampling is performed in O(1) time. However, if the Sum(weights) is large,
// this operation can still be relatively slow due to poor cache locality.
type weightedUniform struct {
indices []int
maxWeight uint64
}
func (s *weightedUniform) Initialize(weights []uint64) error {
totalWeight := uint64(0)
for _, weight := range weights {
newWeight, err := safemath.Add64(totalWeight, weight)
if err != nil {
return err
}
totalWeight = newWeight
}
if totalWeight > s.maxWeight || totalWeight > math.MaxInt32 {
return errWeightsTooLarge
}
size := int(totalWeight)
if size > cap(s.indices) {
s.indices = make([]int, size)
} else {
s.indices = s.indices[:size]
}
offset := 0
for i, weight := range weights {
for j := uint64(0); j < weight; j++ {
s.indices[offset] = i
offset++
}
}
return nil
}
func (s *weightedUniform) Sample(value uint64) (int, bool) {
if uint64(len(s.indices)) <= value {
return 0, false
}
return s.indices[int(value)], true
}
+28
View File
@@ -0,0 +1,28 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
// WeightedWithoutReplacement defines how to sample weight without replacement.
// Note that the behavior is to sample the weight without replacement, not the
// indices. So duplicate indices can be returned.
type WeightedWithoutReplacement interface {
Initialize(weights []uint64) error
Sample(count int) ([]int, bool)
}
// NewDeterministicWeightedWithoutReplacement returns a new sampler
func NewDeterministicWeightedWithoutReplacement(source Source) WeightedWithoutReplacement {
return &weightedWithoutReplacementGeneric{
u: NewDeterministicUniform(source),
w: NewWeighted(),
}
}
// NewWeightedWithoutReplacement returns a new sampler
func NewWeightedWithoutReplacement() WeightedWithoutReplacement {
return &weightedWithoutReplacementGeneric{
u: NewUniform(),
w: NewWeighted(),
}
}
@@ -0,0 +1,37 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"fmt"
"testing"
"github.com/stretchr/testify/require"
)
func BenchmarkWeightedWithoutReplacement(b *testing.B) {
sizes := []int{
1,
5,
25,
50,
75,
100,
}
for _, size := range sizes {
b.Run(fmt.Sprintf("%d elements", size), func(b *testing.B) {
require := require.New(b)
s := NewWeightedWithoutReplacement()
_, weights, err := CalcWeightedPoW(0, 100000)
require.NoError(err)
require.NoError(s.Initialize(weights))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = s.Sample(size)
}
})
}
}
@@ -0,0 +1,42 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import "github.com/luxfi/math"
type weightedWithoutReplacementGeneric struct {
u Uniform
w Weighted
}
func (s *weightedWithoutReplacementGeneric) Initialize(weights []uint64) error {
totalWeight := uint64(0)
for _, weight := range weights {
newWeight, err := math.Add64(totalWeight, weight)
if err != nil {
return err
}
totalWeight = newWeight
}
s.u.Initialize(totalWeight)
return s.w.Initialize(weights)
}
func (s *weightedWithoutReplacementGeneric) Sample(count int) ([]int, bool) {
s.u.Reset()
indices := make([]int, count)
for i := 0; i < count; i++ {
weight, ok := s.u.Next()
if !ok {
return nil, false
}
indices[i], ok = s.w.Sample(weight)
if !ok {
return nil, false
}
}
return indices, true
}
@@ -0,0 +1,87 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package sampler
import (
"math"
"slices"
"testing"
"github.com/stretchr/testify/require"
safemath "github.com/luxfi/math"
)
func TestWeightedWithoutReplacementInitializeOverflow(t *testing.T) {
s := NewWeightedWithoutReplacement()
err := s.Initialize([]uint64{1, math.MaxUint64})
require.ErrorIs(t, err, safemath.ErrOverflow)
}
func TestWeightedWithoutReplacementOutOfRange(t *testing.T) {
require := require.New(t)
s := NewWeightedWithoutReplacement()
require.NoError(s.Initialize([]uint64{1}))
_, ok := s.Sample(2)
require.False(ok)
}
func TestWeightedWithoutReplacementEmptyWithoutWeight(t *testing.T) {
require := require.New(t)
s := NewWeightedWithoutReplacement()
require.NoError(s.Initialize(nil))
indices, ok := s.Sample(0)
require.True(ok)
require.Empty(indices)
}
func TestWeightedWithoutReplacementEmpty(t *testing.T) {
require := require.New(t)
s := NewWeightedWithoutReplacement()
require.NoError(s.Initialize([]uint64{1}))
indices, ok := s.Sample(0)
require.True(ok)
require.Empty(indices)
}
func TestWeightedWithoutReplacementSingleton(t *testing.T) {
require := require.New(t)
s := NewWeightedWithoutReplacement()
require.NoError(s.Initialize([]uint64{1}))
indices, ok := s.Sample(1)
require.True(ok)
require.Equal([]int{0}, indices)
}
func TestWeightedWithoutReplacementWithZero(t *testing.T) {
require := require.New(t)
s := NewWeightedWithoutReplacement()
require.NoError(s.Initialize([]uint64{0, 1}))
indices, ok := s.Sample(1)
require.True(ok)
require.Equal([]int{1}, indices)
}
func TestWeightedWithoutReplacementDistribution(t *testing.T) {
require := require.New(t)
s := NewWeightedWithoutReplacement()
require.NoError(s.Initialize([]uint64{1, 1, 2}))
indices, ok := s.Sample(4)
require.True(ok)
slices.Sort(indices)
require.Equal([]int{0, 1, 2, 2}, indices)
}
+138
View File
@@ -0,0 +1,138 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package setmap
import (
"github.com/luxfi/math/set"
"github.com/luxfi/utils"
)
type Entry[K any, V comparable] struct {
Key K
Set set.Set[V]
}
// SetMap is a map to a set where all sets are non-overlapping.
type SetMap[K, V comparable] struct {
keyToSet map[K]set.Set[V]
valueToKey map[V]K
}
// New creates a new empty setmap.
func New[K, V comparable]() *SetMap[K, V] {
return &SetMap[K, V]{
keyToSet: make(map[K]set.Set[V]),
valueToKey: make(map[V]K),
}
}
// Put the new entry into the map. Removes and returns:
// * The existing entry for [key].
// * Existing entries where the set overlaps with the [set].
func (m *SetMap[K, V]) Put(key K, set set.Set[V]) []Entry[K, V] {
removed := m.DeleteOverlapping(set)
if removedSet, ok := m.DeleteKey(key); ok {
removed = append(removed, Entry[K, V]{
Key: key,
Set: removedSet,
})
}
m.keyToSet[key] = set
for val := range set {
m.valueToKey[val] = key
}
return removed
}
// GetKey that maps to the provided value.
func (m *SetMap[K, V]) GetKey(val V) (K, bool) {
key, ok := m.valueToKey[val]
return key, ok
}
// GetSet that is mapped to by the provided key.
func (m *SetMap[K, V]) GetSet(key K) (set.Set[V], bool) {
val, ok := m.keyToSet[key]
return val, ok
}
// HasKey returns true if [key] is in the map.
func (m *SetMap[K, _]) HasKey(key K) bool {
_, ok := m.keyToSet[key]
return ok
}
// HasValue returns true if [val] is in a set in the map.
func (m *SetMap[_, V]) HasValue(val V) bool {
_, ok := m.valueToKey[val]
return ok
}
// HasOverlap returns true if [set] overlaps with any of the sets in the map.
func (m *SetMap[_, V]) HasOverlap(set set.Set[V]) bool {
if set.Len() < len(m.valueToKey) {
for val := range set {
if _, ok := m.valueToKey[val]; ok {
return true
}
}
} else {
for val := range m.valueToKey {
if set.Contains(val) {
return true
}
}
}
return false
}
// DeleteKey removes [key] from the map and returns the set it mapped to.
func (m *SetMap[K, V]) DeleteKey(key K) (set.Set[V], bool) {
set, ok := m.keyToSet[key]
if !ok {
return nil, false
}
delete(m.keyToSet, key)
for val := range set {
delete(m.valueToKey, val)
}
return set, true
}
// DeleteValue removes and returns the entry that contained [val].
func (m *SetMap[K, V]) DeleteValue(val V) (K, set.Set[V], bool) {
key, ok := m.valueToKey[val]
if !ok {
return utils.Zero[K](), nil, false
}
set, _ := m.DeleteKey(key)
return key, set, true
}
// DeleteOverlapping removes and returns all the entries where the set overlaps
// with [set].
func (m *SetMap[K, V]) DeleteOverlapping(set set.Set[V]) []Entry[K, V] {
var removed []Entry[K, V]
for val := range set {
if k, removedSet, ok := m.DeleteValue(val); ok {
removed = append(removed, Entry[K, V]{
Key: k,
Set: removedSet,
})
}
}
return removed
}
// Len return the number of sets in the map.
func (m *SetMap[K, V]) Len() int {
return len(m.keyToSet)
}
// LenValues return the total number of values across all sets in the map.
func (m *SetMap[K, V]) LenValues() int {
return len(m.valueToKey)
}
+450
View File
@@ -0,0 +1,450 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package setmap
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/luxfi/math/set"
)
func TestSetMapPut(t *testing.T) {
tests := []struct {
name string
state *SetMap[int, int]
key int
value set.Set[int]
expectedRemoved []Entry[int, int]
expectedState *SetMap[int, int]
}{
{
name: "none removed",
state: New[int, int](),
key: 1,
value: set.Of(2),
expectedRemoved: nil,
expectedState: &SetMap[int, int]{
keyToSet: map[int]set.Set[int]{
1: set.Of(2),
},
valueToKey: map[int]int{
2: 1,
},
},
},
{
name: "key removed",
state: &SetMap[int, int]{
keyToSet: map[int]set.Set[int]{
1: set.Of(2),
},
valueToKey: map[int]int{
2: 1,
},
},
key: 1,
value: set.Of(3),
expectedRemoved: []Entry[int, int]{
{
Key: 1,
Set: set.Of(2),
},
},
expectedState: &SetMap[int, int]{
keyToSet: map[int]set.Set[int]{
1: set.Of(3),
},
valueToKey: map[int]int{
3: 1,
},
},
},
{
name: "value removed",
state: &SetMap[int, int]{
keyToSet: map[int]set.Set[int]{
1: set.Of(2),
},
valueToKey: map[int]int{
2: 1,
},
},
key: 3,
value: set.Of(2),
expectedRemoved: []Entry[int, int]{
{
Key: 1,
Set: set.Of(2),
},
},
expectedState: &SetMap[int, int]{
keyToSet: map[int]set.Set[int]{
3: set.Of(2),
},
valueToKey: map[int]int{
2: 3,
},
},
},
{
name: "key and value removed",
state: &SetMap[int, int]{
keyToSet: map[int]set.Set[int]{
1: set.Of(2),
3: set.Of(4),
},
valueToKey: map[int]int{
2: 1,
4: 3,
},
},
key: 1,
value: set.Of(4),
expectedRemoved: []Entry[int, int]{
{
Key: 1,
Set: set.Of(2),
},
{
Key: 3,
Set: set.Of(4),
},
},
expectedState: &SetMap[int, int]{
keyToSet: map[int]set.Set[int]{
1: set.Of(4),
},
valueToKey: map[int]int{
4: 1,
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
removed := test.state.Put(test.key, test.value)
require.ElementsMatch(test.expectedRemoved, removed)
require.Equal(test.expectedState, test.state)
})
}
}
func TestSetMapHasValueAndGetKeyAndSetOverlaps(t *testing.T) {
m := New[int, int]()
require.Empty(t, m.Put(1, set.Of(2)))
tests := []struct {
name string
value int
expectedKey int
expectedExists bool
}{
{
name: "fetch unknown",
value: 3,
expectedKey: 0,
expectedExists: false,
},
{
name: "fetch known value",
value: 2,
expectedKey: 1,
expectedExists: true,
},
{
name: "fetch known key",
value: 1,
expectedKey: 0,
expectedExists: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
exists := m.HasValue(test.value)
require.Equal(test.expectedExists, exists)
key, exists := m.GetKey(test.value)
require.Equal(test.expectedKey, key)
require.Equal(test.expectedExists, exists)
})
}
}
func TestSetMapHasOverlap(t *testing.T) {
m := New[int, int]()
require.Empty(t, m.Put(1, set.Of(2)))
require.Empty(t, m.Put(2, set.Of(3, 4)))
tests := []struct {
name string
set set.Set[int]
expectedOverlaps bool
}{
{
name: "small fetch unknown",
set: set.Of(5),
expectedOverlaps: false,
},
{
name: "large fetch unknown",
set: set.Of(5, 6, 7, 8),
expectedOverlaps: false,
},
{
name: "small fetch known",
set: set.Of(3),
expectedOverlaps: true,
},
{
name: "large fetch known",
set: set.Of(3, 5, 6, 7, 8),
expectedOverlaps: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
overlaps := m.HasOverlap(test.set)
require.Equal(t, test.expectedOverlaps, overlaps)
})
}
}
func TestSetMapHasKeyAndGetSet(t *testing.T) {
m := New[int, int]()
require.Empty(t, m.Put(1, set.Of(2)))
tests := []struct {
name string
key int
expectedValue set.Set[int]
expectedExists bool
}{
{
name: "fetch unknown",
key: 3,
expectedValue: nil,
expectedExists: false,
},
{
name: "fetch known key",
key: 1,
expectedValue: set.Of(2),
expectedExists: true,
},
{
name: "fetch known value",
key: 2,
expectedValue: nil,
expectedExists: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
exists := m.HasKey(test.key)
require.Equal(test.expectedExists, exists)
value, exists := m.GetSet(test.key)
require.Equal(test.expectedValue, value)
require.Equal(test.expectedExists, exists)
})
}
}
func TestSetMapDeleteKey(t *testing.T) {
tests := []struct {
name string
state *SetMap[int, int]
key int
expectedValue set.Set[int]
expectedRemoved bool
expectedState *SetMap[int, int]
}{
{
name: "none removed",
state: New[int, int](),
key: 1,
expectedValue: nil,
expectedRemoved: false,
expectedState: New[int, int](),
},
{
name: "key removed",
state: &SetMap[int, int]{
keyToSet: map[int]set.Set[int]{
1: set.Of(2),
},
valueToKey: map[int]int{
2: 1,
},
},
key: 1,
expectedValue: set.Of(2),
expectedRemoved: true,
expectedState: New[int, int](),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
value, removed := test.state.DeleteKey(test.key)
require.Equal(test.expectedValue, value)
require.Equal(test.expectedRemoved, removed)
require.Equal(test.expectedState, test.state)
})
}
}
func TestSetMapDeleteValue(t *testing.T) {
tests := []struct {
name string
state *SetMap[int, int]
value int
expectedKey int
expectedSet set.Set[int]
expectedRemoved bool
expectedState *SetMap[int, int]
}{
{
name: "none removed",
state: New[int, int](),
value: 1,
expectedKey: 0,
expectedSet: nil,
expectedRemoved: false,
expectedState: New[int, int](),
},
{
name: "key removed",
state: &SetMap[int, int]{
keyToSet: map[int]set.Set[int]{
1: set.Of(2),
},
valueToKey: map[int]int{
2: 1,
},
},
value: 2,
expectedKey: 1,
expectedSet: set.Of(2),
expectedRemoved: true,
expectedState: New[int, int](),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
key, set, removed := test.state.DeleteValue(test.value)
require.Equal(test.expectedKey, key)
require.Equal(test.expectedSet, set)
require.Equal(test.expectedRemoved, removed)
require.Equal(test.expectedState, test.state)
})
}
}
func TestSetMapDeleteOverlapping(t *testing.T) {
tests := []struct {
name string
state *SetMap[int, int]
set set.Set[int]
expectedRemoved []Entry[int, int]
expectedState *SetMap[int, int]
}{
{
name: "none removed",
state: New[int, int](),
set: set.Of(1),
expectedRemoved: nil,
expectedState: New[int, int](),
},
{
name: "key removed",
state: &SetMap[int, int]{
keyToSet: map[int]set.Set[int]{
1: set.Of(2),
},
valueToKey: map[int]int{
2: 1,
},
},
set: set.Of(2),
expectedRemoved: []Entry[int, int]{
{
Key: 1,
Set: set.Of(2),
},
},
expectedState: New[int, int](),
},
{
name: "multiple keys removed",
state: &SetMap[int, int]{
keyToSet: map[int]set.Set[int]{
1: set.Of(2, 3),
2: set.Of(4),
},
valueToKey: map[int]int{
2: 1,
3: 1,
4: 2,
},
},
set: set.Of(2, 4),
expectedRemoved: []Entry[int, int]{
{
Key: 1,
Set: set.Of(2, 3),
},
{
Key: 2,
Set: set.Of(4),
},
},
expectedState: New[int, int](),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
removed := test.state.DeleteOverlapping(test.set)
require.ElementsMatch(test.expectedRemoved, removed)
require.Equal(test.expectedState, test.state)
})
}
}
func TestSetMapLen(t *testing.T) {
require := require.New(t)
m := New[int, int]()
require.Zero(m.Len())
require.Zero(m.LenValues())
m.Put(1, set.Of(2))
require.Equal(1, m.Len())
require.Equal(1, m.LenValues())
m.Put(2, set.Of(3, 4))
require.Equal(2, m.Len())
require.Equal(3, m.LenValues())
m.Put(1, set.Of(4, 5))
require.Equal(1, m.Len())
require.Equal(2, m.LenValues())
m.DeleteKey(1)
require.Zero(m.Len())
require.Zero(m.LenValues())
}
+118
View File
@@ -0,0 +1,118 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package window
import (
"sync"
"time"
"github.com/luxfi/utils"
"github.com/luxfi/container/buffer"
"github.com/luxfi/timer/mockable"
)
var _ Window[struct{}] = (*window[struct{}])(nil)
// Window is an interface which represents a sliding window of elements.
type Window[T any] interface {
Add(value T)
Oldest() (T, bool)
Length() int
}
type window[T any] struct {
// mocked clock for unit testing
clock *mockable.Clock
// time-to-live for elements in the window
ttl time.Duration
// max amount of elements allowed in the window
maxSize int
// min amount of elements required in the window before allowing removal
// based on time
minSize int
// mutex for synchronization
lock sync.Mutex
// elements in the window
elements buffer.Deque[node[T]]
}
// Config exposes parameters for Window
type Config struct {
Clock *mockable.Clock
MaxSize int
MinSize int
TTL time.Duration
}
// New returns an instance of window
func New[T any](config Config) Window[T] {
return &window[T]{
clock: config.Clock,
ttl: config.TTL,
maxSize: config.MaxSize,
minSize: config.MinSize,
elements: buffer.NewUnboundedDeque[node[T]](config.MaxSize + 1),
}
}
// Add adds an element to a window and also evicts any elements if they've been
// present in the window beyond the configured time-to-live
func (w *window[T]) Add(value T) {
w.lock.Lock()
defer w.lock.Unlock()
// add the new block id
w.elements.PushRight(node[T]{
value: value,
entryTime: w.clock.Time(),
})
w.removeStaleNodes()
if w.elements.Len() > w.maxSize {
_, _ = w.elements.PopLeft()
}
}
// Oldest returns the oldest element in the window.
func (w *window[T]) Oldest() (T, bool) {
w.lock.Lock()
defer w.lock.Unlock()
w.removeStaleNodes()
oldest, ok := w.elements.PeekLeft()
if !ok {
return utils.Zero[T](), false
}
return oldest.value, true
}
// Length returns the number of elements in the window.
func (w *window[T]) Length() int {
w.lock.Lock()
defer w.lock.Unlock()
w.removeStaleNodes()
return w.elements.Len()
}
// removeStaleNodes removes any nodes beyond the configured ttl of a window node.
func (w *window[T]) removeStaleNodes() {
// If we're beyond the expiry threshold, removeStaleNodes this node from our
// window. Nodes are guaranteed to be strictly increasing in entry time,
// so we can break this loop once we find the first non-stale one.
for w.elements.Len() > w.minSize {
oldest, ok := w.elements.PeekLeft()
if !ok || w.clock.Time().Sub(oldest.entryTime) <= w.ttl {
return
}
_, _ = w.elements.PopLeft()
}
}
// helper struct to represent elements in the window
type node[T any] struct {
value T
entryTime time.Time
}
+287
View File
@@ -0,0 +1,287 @@
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package window
import (
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/luxfi/timer/mockable"
)
const (
testTTL = 10 * time.Second
testMaxSize = 10
)
// TestAdd tests that elements are populated as expected, ignoring
// any eviction.
func TestAdd(t *testing.T) {
tests := []struct {
name string
window []int
newlyAdded int
expectedOldest int
}{
{
name: "empty",
window: []int{},
newlyAdded: 1,
expectedOldest: 1,
},
{
name: "populated",
window: []int{1, 2, 3, 4},
newlyAdded: 5,
expectedOldest: 1,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require := require.New(t)
clock := &mockable.Clock{}
clock.Set(time.Now())
window := New[int](
Config{
Clock: clock,
MaxSize: testMaxSize,
TTL: testTTL,
},
)
for _, element := range test.window {
window.Add(element)
}
window.Add(test.newlyAdded)
require.Equal(len(test.window)+1, window.Length())
oldest, ok := window.Oldest()
require.True(ok)
require.Equal(test.expectedOldest, oldest)
})
}
}
// TestTTLAdd tests the case where an element is stale in the window
// and needs to be evicted on Add.
func TestTTLAdd(t *testing.T) {
require := require.New(t)
clock := &mockable.Clock{}
start := time.Now()
clock.Set(start)
window := New[int](
Config{
Clock: clock,
MaxSize: testMaxSize,
TTL: testTTL,
},
)
// Now the window looks like this:
// [1, 2, 3]
window.Add(1)
window.Add(2)
window.Add(3)
require.Equal(3, window.Length())
oldest, ok := window.Oldest()
require.True(ok)
require.Equal(1, oldest)
// Now we're one second past the ttl of 10 seconds as defined in testTTL,
// so all existing elements need to be evicted.
clock.Set(start.Add(testTTL + time.Second))
// Now the window should look like this:
// [4]
window.Add(4)
require.Equal(1, window.Length())
oldest, ok = window.Oldest()
require.True(ok)
require.Equal(4, oldest)
// Now we're one second before the ttl of 10 seconds of when [4] was added,
// no element should be evicted
// [4, 5]
clock.Set(start.Add(2 * testTTL))
window.Add(5)
require.Equal(2, window.Length())
oldest, ok = window.Oldest()
require.True(ok)
require.Equal(4, oldest)
// Now the window is still containing 4:
// [4, 5]
// we only evict on Add method because the window is calculated in the last element added
require.Equal(2, window.Length())
oldest, ok = window.Oldest()
require.True(ok)
require.Equal(4, oldest)
}
// TestTTLLength tests that elements are evicted on Length
func TestTTLLength(t *testing.T) {
require := require.New(t)
clock := &mockable.Clock{}
start := time.Now()
clock.Set(start)
window := New[int](
Config{
Clock: clock,
MaxSize: testMaxSize,
TTL: testTTL,
},
)
// Now the window looks like this:
// [1, 2, 3]
window.Add(1)
window.Add(2)
window.Add(3)
require.Equal(3, window.Length())
// Now we're one second past the ttl of 10 seconds as defined in testTTL,
// so all existing elements need to be evicted.
clock.Set(start.Add(testTTL + time.Second))
// No more elements should be present in the window.
require.Equal(0, window.Length())
}
// TestTTLOldest tests that stale elements are evicted on calling Oldest
func TestTTLOldest(t *testing.T) {
require := require.New(t)
clock := &mockable.Clock{}
start := time.Now()
clock.Set(start)
windowIntf := New[int](
Config{
Clock: clock,
MaxSize: testMaxSize,
TTL: testTTL,
},
)
require.IsType(&window[int]{}, windowIntf)
window := windowIntf.(*window[int])
// Now the window looks like this:
// [1, 2, 3]
window.Add(1)
window.Add(2)
window.Add(3)
oldest, ok := window.Oldest()
require.True(ok)
require.Equal(1, oldest)
require.Equal(3, window.elements.Len())
// Now we're one second before the ttl of 10 seconds as defined in testTTL,
// so all existing elements shoud still exist.
// Add 4 to the window to make it:
// [1, 2, 3, 4]
clock.Set(start.Add(testTTL - time.Second))
window.Add(4)
oldest, ok = window.Oldest()
require.True(ok)
require.Equal(1, oldest)
require.Equal(4, window.elements.Len())
// Now we're one second past the ttl of the initial 3 elements
// call to oldest should now evict 1,2,3 and return 4.
clock.Set(start.Add(testTTL + time.Second))
oldest, ok = window.Oldest()
require.True(ok)
require.Equal(4, oldest)
require.Equal(1, window.elements.Len())
}
// Tests that we bound the amount of elements in the window
func TestMaxCapacity(t *testing.T) {
require := require.New(t)
clock := &mockable.Clock{}
clock.Set(time.Now())
window := New[int](
Config{
Clock: clock,
MaxSize: 3,
TTL: testTTL,
},
)
// Now the window looks like this:
// [1, 2, 3]
window.Add(1)
window.Add(2)
window.Add(3)
// We should evict 1 and replace it with 4.
// Now the window should look like this:
// [2, 3, 4]
window.Add(4)
// We should evict 2 and replace it with 5.
// Now the window should look like this:
// [3, 4, 5]
window.Add(5)
// We should evict 3 and replace it with 6.
// Now the window should look like this:
// [4, 5, 6]
window.Add(6)
require.Equal(3, window.Length())
oldest, ok := window.Oldest()
require.True(ok)
require.Equal(4, oldest)
}
// Tests that we do not evict past the minimum window size
func TestMinCapacity(t *testing.T) {
require := require.New(t)
clock := &mockable.Clock{}
start := time.Now()
clock.Set(start)
window := New[int](
Config{
Clock: clock,
MaxSize: 3,
MinSize: 2,
TTL: testTTL,
},
)
// Now the window looks like this:
// [1, 2, 3]
window.Add(1)
window.Add(2)
window.Add(3)
clock.Set(start.Add(testTTL + time.Second))
// All of [1, 2, 3] are past the ttl now, but we don't evict 3 because of
// the minimum length.
// Now the window should look like this:
// [3, 4]
window.Add(4)
require.Equal(2, window.Length())
oldest, ok := window.Oldest()
require.True(ok)
require.Equal(3, oldest)
}