Compare commits

...

2 Commits

Author SHA1 Message Date
2cf1847dca feat: documentation 2026-04-05 15:01:17 -04:00
895daf6002 feat: starting implementation 2026-04-05 15:01:04 -04:00
5 changed files with 458 additions and 0 deletions

235
.golangci.yml Normal file
View File

@@ -0,0 +1,235 @@
---
# golangci-lint configuration file made by @ccoVeille
# Source: https://github.com/ccoVeille/golangci-lint-config-examples/
# Author: @ccoVeille
# License: MIT
# Variant: 03-safe
# Version: v2.0.0
#
version: "2"
formatters:
enable:
# format the code
- gofmt
# format the block of imports
- gci
settings:
# format the code with Go standard library
gofmt:
# simplify the code
# https://pkg.go.dev/cmd/gofmt#hdr-The_simplify_command
simplify: true
rewrite-rules:
# replace `interface{}` with `any` in the code on format
- pattern: 'interface{}'
replacement: 'any'
# make sure imports are always in a deterministic order
# https://github.com/daixiang0/gci/
gci: # define the section orders for imports
sections:
# Standard section: captures all standard packages.
- standard
# Default section: catchall that is not standard or custom
- default
# linters that related to local tool, so they should be separated
- localmodule
linters:
exclusions:
# these presets where present in the v1 version of golangci-lint
# it's interesting to keep them when migrating, but removing them should be the goal
presets:
# exclude check on comments format in godoc
# These are common false positives in poor code
# you should not use this on recent code you write from scratch
# More information: https://golangci-lint.run/usage/false-positives/#comments
#
# Please uncomment the following line if your code is not using the godoc format
# - comments
# Common false positives
# feel free to remove this if you don't have any false positives
# More information: https://golangci-lint.run/usage/false-positives/#common-false-positives
- common-false-positives
# Legacy preset is not recommended anymore
# More information: https://golangci-lint.run/usage/false-positives/#legacy
- legacy
# std-error-handling is a set of rules that avoid reporting unhandled errors on common functions/methods
# More information: https://golangci-lint.run/usage/false-positives/#std-error-handling
- std-error-handling
# some linters are enabled by default
# https://golangci-lint.run/usage/linters/
#
# enable some extra linters
enable:
# Errcheck is a program for checking for unchecked errors in Go code.
- errcheck
# Vet examines Go source code and reports suspicious constructs.
- govet
# Detects when assignments to existing variables are not used.
- ineffassign
# It's a set of rules from staticcheck. See https://staticcheck.io/
- staticcheck
# Checks Go code for unused constants, variables, functions and types.
- unused
# Fast, configurable, extensible, flexible, and beautiful linter for Go.
# Drop-in replacement of golint.
- revive
# make sure to use t.Helper() when needed
- thelper
# mirror suggests rewrites to avoid unnecessary []byte/string conversion
- mirror
# detect the possibility to use variables/constants from the Go standard library.
- usestdlibvars
# Finds commonly misspelled English words.
- misspell
# Checks for duplicate words in the source code.
- dupword
# linter to detect errors invalid key values count
- loggercheck
# detect when a package or method could be replaced by one from the standard library
- exptostd
# detects nested contexts in loops or function literals
- fatcontext
# Reports uses of functions with replacement inside the testing package.
- usetesting
settings:
revive:
rules:
# these are the default revive rules
# you can remove the whole "rules" node if you want
# BUT
# ! /!\ they all need to be present when you want to add more rules than the default ones
# otherwise, you won't have the default rules, but only the ones you define in the "rules" node
# Blank import should be only in a main or test package, or have a comment justifying it.
- name: blank-imports
# Packages should have comments of the form "Package x ...".
- name: package-comments
# context.Context() should be the first parameter of a function when provided as argument.
- name: context-as-argument
arguments:
- allowTypesBefore: "*testing.T"
# Basic types should not be used as a key in `context.WithValue`
- name: context-keys-type
# Importing with `.` makes the programs much harder to understand
- name: dot-imports
# Empty blocks make code less readable and could be a symptom of a bug or unfinished refactoring.
- name: empty-block
# for better readability, variables of type `error` must be named with the prefix `err`.
- name: error-naming
# for better readability, the errors should be last in the list of returned values by a function.
- name: error-return
# for better readability, error messages should not be capitalized or end with punctuation or a newline.
- name: error-strings
# report when replacing `errors.New(fmt.Sprintf())` with `fmt.Errorf()` is possible
- name: errorf
# check naming and commenting conventions on exported symbols.
- name: exported
arguments:
# make error messages clearer
- "sayRepetitiveInsteadOfStutters"
# require comments on public interface methods
- "checkPublicInterface"
# incrementing an integer variable by 1 is recommended to be done using the `++` operator
- name: increment-decrement
# highlights redundant else-blocks that can be eliminated from the code
# - name: indent-error-flow
# This rule suggests a shorter way of writing ranges that do not use the second value.
- name: range
# receiver names in a method should reflect the struct name (p for Person, for example)
- name: receiver-naming
# redefining built in names (true, false, append, make) can lead to bugs very difficult to detect.
- name: redefines-builtin-id
# redundant else-blocks that can be eliminated from the code.
# - name: superfluous-else
# prevent confusing name for variables when using `time` package
- name: time-naming
# warns when an exported function or method returns a value of an un-exported type.
- name: unexported-return
# spots and proposes to remove unreachable code. also helps to spot errors
- name: unreachable-code
# Functions or methods with unused parameters can be a symptom of an unfinished refactoring or a bug.
- name: unused-parameter
# report when a variable declaration can be simplified
- name: var-declaration
# warns when initialism, variable or package naming conventions are not followed.
- name: var-naming
misspell:
# Correct spellings using locale preferences for US or UK.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
# Default ("") is to use a neutral variety of English.
locale: US
# List of words to ignore
# among the one defined in https://github.com/golangci/misspell/blob/master/words.go
ignore-rules: []
# - valor
# - and
# Extra word corrections.
extra-words: []
# - typo: "whattever"
# correction: "whatever"
output:
# Order to use when sorting results.
# Possible values: `file`, `linter`, and `severity`.
#
# If the severity values are inside the following list, they are ordered in this order:
# 1. error
# 2. warning
# 3. high
# 4. medium
# 5. low
# Either they are sorted alphabetically.
#
# Default: ["file"]
sort-order:
- linter
- severity
- file # filepath, line, and column.

2
doc.go Normal file
View File

@@ -0,0 +1,2 @@
// Package ring implements a generic deque using a circular buffer.
package ring

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module git.maximhutz.com/tools/go-ring
go 1.26.1

45
options.go Normal file
View File

@@ -0,0 +1,45 @@
package ring
import "fmt"
// DefaultGrowthFactor is the default multiplier a [Ring] uses to increase its
// capacity. Most implementations use 2.
const DefaultGrowthFactor = 2
// DefaultCapacity is the default starting capacity of a new [Ring]. Sixteen was
// chosen as a reasonable default.
const DefaultCapacity = 16
// A config holds all values that modify the behavior of a [Ring].
type config struct {
capacity uint64
growthFactor uint64
}
// An Option can be used with [New] to modify the behavior of a [Ring].
type Option func(*config)
// GrowthFactor modifies the multiplier used to increase (and decrease) the
// capacity of a [Ring].
// Its value must be greater than 1.
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)
}
}
// Capacity modifies the starting capacity of a [Ring].
// Its value must be non-negative.
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)
}
}

173
ring.go Normal file
View File

@@ -0,0 +1,173 @@
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'.
}
// Cap returns the capacity of 'r'.
func (r Ring[T]) Cap() int {
return len(r.data)
}
// Len returns the number of values in 'r'.
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]
}
// Get returns the value at a certain 'index' in 'r'.
// The index must be within the range of the ring.
func (r Ring[T]) Get(index int) T {
return r.fetch(uint64(index))
}
// Front returns the first value in 'r'.
// The ring must not be empty.
func (r Ring[T]) Front() T {
return r.fetch(0)
}
// Back returns the last value in 'r'.
// The ring must not be empty.
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 capacity of 'r' by its growth factor.
// The ring must be [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,
}
}