From 895daf6002e0322967515b4f9909f4a7b02ed1ef Mon Sep 17 00:00:00 2001 From: "M.V. Hutz" Date: Sat, 4 Apr 2026 17:29:49 +0200 Subject: [PATCH] feat: starting implementation --- go.mod | 3 + options.go | 34 +++++++++++ ring.go | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 go.mod create mode 100644 options.go create mode 100644 ring.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..acf64c4 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.maximhutz.com/tools/go-ring + +go 1.26.1 diff --git a/options.go b/options.go new file mode 100644 index 0000000..b795dc1 --- /dev/null +++ b/options.go @@ -0,0 +1,34 @@ +package ring + +import "fmt" + +const DefaultGrowthFactor = 2 + +const DefaultCapacity = 16 + +type config struct { + capacity uint64 + growthFactor uint64 +} + +type Option func(*config) + +func GrowthFactor(value int) Option { + if value <= 1 { + panic(fmt.Errorf("go-ring: growth factor must be greater than 1, got %d", value)) + } + + return func(c *config) { + c.growthFactor = uint64(value) + } +} + +func Capacity(value int) Option { + if value < 0 { + panic(fmt.Errorf("go-ring: starting capacity must be non-negative, got %d", value)) + } + + return func(c *config) { + c.capacity = uint64(value) + } +} diff --git a/ring.go b/ring.go new file mode 100644 index 0000000..33962d4 --- /dev/null +++ b/ring.go @@ -0,0 +1,164 @@ +package ring + +import ( + "iter" +) + +// A Ring is a deque, implemented using a circular buffer. +type Ring[T any] struct { + data []T // Is not 'nil'. + growthFactor uint64 // Is greater than 1. + size uint64 // Is not greater than 'len(data)'. + offset uint64 // Is not greater than 'size'. +} + +func (r Ring[T]) Cap() int { + return len(r.data) +} + +func (r Ring[T]) Len() int { + return int(r.size) +} + +// fetch returns the value of a certain index. The index MUST NOT be out of +// range. +func (r Ring[T]) fetch(index uint64) T { + if index >= r.size { + panic("Out of range!") + } + + return r.data[(r.offset+index)%r.size] +} + +func (r Ring[T]) Get(index int) T { + return r.fetch(uint64(index)) +} + +func (r Ring[T]) Front() T { + return r.fetch(0) +} + +func (r Ring[T]) Back() T { + return r.fetch(r.size - 1) +} + +// resize replaces the current data table with a new table a new 'capacity'. The +// new capacity MUST NOT be less than [Ring.Len]. +func (r *Ring[T]) resize(capacity uint64) { + if capacity < r.size { + panic("Resize too small!") + } + + newData := make([]T, capacity) + for i, value := range r.Entries() { + newData[i] = value + } + + r.offset = 0 + r.data = newData +} + +// grow increases the size of the array by its growth factor. It is guaranteed +// to not be [Ring.full]. +func (r *Ring[T]) grow() { + if r.size == 0 { + r.resize(1) + } else { + r.resize(r.size * r.growthFactor) + } +} + +// full returns true if every slot in the data table is full. +func (r Ring[T]) full() bool { + return r.size == uint64(len(r.data)) +} + +// shrink decreases the size of the data table by its growth factor. Do not call +// this when the data table is not [Ring.sparse]. +func (r *Ring[T]) shrink() { + r.resize(r.size / r.growthFactor) +} + +// space returns true if the data table is empty enough to shrink. +func (r *Ring[T]) sparse() bool { + return r.size*r.growthFactor < uint64(len(r.data)) +} + +// set updates the value of a certain index. The index MUST be in range. +func (r Ring[T]) set(index uint64, value T) { + if index >= r.size { + panic("Out of range!") + } + + r.data[(r.offset+index)%r.size] = value +} + +func (r *Ring[T]) PushBack(value T) { + if r.full() { + r.grow() + } + + r.size++ + r.set(r.size-1, value) +} + +func (r *Ring[T]) PopBack() T { + value := r.fetch(r.size - 1) + r.size-- + + if r.sparse() { + r.shrink() + } + + return value +} + +func (r *Ring[T]) PushFront(value T) { + if r.full() { + r.grow() + } + + r.offset-- + r.size++ + r.set(0, value) +} + +func (r *Ring[T]) PopFront() T { + value := r.fetch(0) + r.size-- + r.offset++ + + if r.sparse() { + r.shrink() + } + + return value +} + +func (r Ring[T]) Entries() iter.Seq2[int, T] { + return func(yield func(int, T) bool) { + for i := range r.size { + if !yield(int(i), r.fetch(i)) { + return + } + } + } +} + +func New[T any](options ...Option) *Ring[T] { + config := &config{ + growthFactor: DefaultCapacity, + capacity: DefaultCapacity, + } + + for _, option := range options { + option(config) + } + + return &Ring[T]{ + data: make([]T, config.capacity), + offset: 0, + size: 0, + growthFactor: config.growthFactor, + } +}