mirror of
https://github.com/luxfi/container.git
synced 2026-09-24 00:24:34 +00:00
feat: add data structures from utils (iterator, buffer, sampler, etc)
This commit is contained in:
+154
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -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() {}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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))),
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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() {}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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++
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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++
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user