sq/libsq/core/tailbuf/tailbuf.go

364 lines
10 KiB
Go
Raw Normal View History

// Package tailbuf contains a tail buffer [Buf] of fixed size that provides a
// window on the tail of the items written via [Buf.Write]. Start with
// [tailbuf.New] to create a [Buf].
package tailbuf
import (
"context"
)
// Buf is an append-only fixed-size circular buffer that provides a window on
// the tail of items written to the buffer. The zero value is technically
// usable, but not very useful. Instead, invoke [tailbuf.New] to create a Buf.
// Buf is not safe for concurrent use.
//
// Note the terms "nominal buffer" and "tail window" (or just "window"). The
// nominal buffer is the complete list of items written to Buf via the Buf.Write
// or Buf.WriteAll methods. However, Buf drops the oldest items as it fills
// (which is the entire point of this package): the tail window is the subset of
// the nominal buffer that is currently available. Some of Buf's methods take
2024-03-04 09:21:10 +03:00
// arguments that are indices into the nominal buffer, for example
// [SliceNominal].
type Buf[T any] struct {
// window is the circular buffer.
window []T
// back is the cursor for the oldest item.
back int
// front is the cursor for the newest item.
front int
// count is the number of items written.
count int
}
// New returns a new Buf with the specified capacity. It panics if capacity is
// less than 1.
func New[T any](capacity int) *Buf[T] {
if capacity < 0 {
panic("capacity must be >= 0") // FIXME: make zero value usable
}
return &Buf[T]{
window: make([]T, capacity),
back: -1,
front: -1,
}
}
2024-03-04 09:21:10 +03:00
// Write appends t to the buffer. If the buffer fills, the oldest item is
// overwritten. The buffer is returned for chaining.
func (b *Buf[T]) Write(t T) *Buf[T] {
if len(b.window) == 0 {
// We won't actually store the item, but we still count it.
b.count++
return b
}
b.write(t)
return b
}
// WriteAll appends items to the buffer. If the buffer fills, the oldest items
// are overwritten. The buffer is returned for chaining.
func (b *Buf[T]) WriteAll(a ...T) *Buf[T] {
if len(b.window) == 0 {
// We won't actually store the items, but we still count them.
b.count += len(a)
return b
}
for i := range a {
b.write(a[i])
}
return b
}
// write appends item [Buf]. If the buffer is full, the oldest item is
// overwritten.
func (b *Buf[T]) write(item T) {
b.count++
switch {
case b.front == -1:
b.back = 0
case b.count > len(b.window):
b.back = (b.back + 1) % len(b.window)
default:
}
b.front = (b.front + 1) % len(b.window)
b.window[b.front] = item
}
// Tail returns a slice containing the current tail window of items in the
// buffer, with the oldest item at index 0. Depending on the state of Buf, the
// returned slice may be a slice of Buf's internal data, or a copy. Thus you
2024-03-04 09:21:10 +03:00
// should copy the returned slice before modifying it, or instead use
// [SliceTail].
func (b *Buf[T]) Tail() []T {
switch {
case len(b.window) == 0, b.count < 1:
return make([]T, 0) // REVISIT: why not return a nil slice?
case b.count <= len(b.window):
return b.window[0:b.count]
case b.front >= b.back:
return b.window[b.back : b.front+1]
default:
s := make([]T, 0, len(b.window))
s = append(s, b.window[b.back:]...)
return append(s, b.window[:b.front+1]...)
}
}
// Count returns the total number of items written to the buffer.
func (b *Buf[T]) Count() int {
return b.count
}
// InBounds returns true if the index i of the nominal buffer is within the
// bounds of the tail window. That is to say, InBounds returns true if the ith
// item written to the buffer is still in the tail window.
func (b *Buf[T]) InBounds(i int) bool {
if b.count == 0 || i < 0 {
return false
}
start, end := b.Bounds()
return i >= start && i <= end // TODO: should be < end?
}
// Bounds returns the start and end indices of the tail window vs the nominal
// buffer. If the buffer is empty, start and end are both 0. The returned
// values are the same as [Buf.Offset] and [Buf.Count].
func (b *Buf[T]) Bounds() (start, end int) {
return b.Offset(), b.Count()
}
// Capacity returns the capacity of Buf, which is the fixed size specified when
// the buffer was created.
func (b *Buf[T]) Capacity() int {
return len(b.window)
}
// Reset resets the buffer to its initial state. The buffer is returned for
// chaining.
func (b *Buf[T]) Reset() *Buf[T] {
b.back = -1
b.front = -1
b.count = 0
return b
}
// Offset returns the offset of the current window vs the nominal complete list
// of items written to the buffer. It is effectively the count of items that
// have slipped out of the tail window. If the buffer is empty, the returned
// offset is 0.
func (b *Buf[T]) Offset() int {
if b.count <= len(b.window) {
return 0
}
return b.count - len(b.window)
}
// Front returns the newest item in the tail window. If Buf is empty, the zero
// value of T is returned.
func (b *Buf[T]) Front() T {
if b.front == -1 {
var t T
return t
}
return b.window[b.front]
}
// Back returns the oldest item in the tail window. If Buf empty, the zero value
// of T is returned.
func (b *Buf[T]) Back() T {
if b.back == -1 {
var t T
return t
}
return b.window[b.back]
}
// Apply applies fn to each item in the tail window, in oldest-to-newest order.
// If Buf is empty, fn is not invoked. The buffer is returned for chaining.
//
// buf := tailbuf.New[string](3)
// buf.WriteAll("a", "b ", " c ")
// buf.Apply(strings.TrimSpace).Apply(strings.ToUpper)
// fmt.Println(buf.Tail())
// // Output: [A B C]
//
// Using Apply is cheaper than getting the slice via [Buf.Tail] and applying fn
// manually, as it avoids the possible allocation of a new slice by Buf.Tail.
//
// For more control, or to handle errors, use [Buf.Do].
func (b *Buf[T]) Apply(fn func(item T) T) *Buf[T] {
if b.count == 0 {
return b
}
if b.front > b.back {
for i := b.back; i <= b.front; i++ {
b.window[i] = fn(b.window[i])
}
return b
}
for i := b.back; i < len(b.window); i++ {
b.window[i] = fn(b.window[i])
}
for i := 0; i <= b.front; i++ {
b.window[i] = fn(b.window[i])
}
return b
}
// Do applies fn to each item in the tail window, in oldest-to-newest order,
// replacing each item with the value returned by successful invocation of fn.
// If fn returns an error, the item is not replaced. Execution is halted if any
// invocation of fn returns an error, and that error is returned to the caller.
// Thus a partial application of fn may occur.
//
// If Buf is empty, fn is not invoked.
//
// The index arg to fn is the index of the item in the tail window. You can use
// the offset arg to compute the index of the item in the nominal buffer.
//
// nominalIndex := index + offset
//
// REVISIT: Should index be the tailIndex instead?
//
// The context is not checked for cancellation between iterations. The context
// should be checked in fn if desired.
func (b *Buf[T]) Do(ctx context.Context, fn func(ctx context.Context, item T, index, offset int) (T, error)) error {
if b.count == 0 {
return nil
}
var v T
var err error
if b.front > b.back {
for i := b.back; i <= b.front; i++ {
v, err = fn(ctx, b.window[i], i, i-b.back)
if err != nil {
return err
}
b.window[i] = v
}
return nil
}
for i := b.back; i < len(b.window); i++ {
v, err = fn(ctx, b.window[i], i, i-b.back)
if err != nil {
return err
}
b.window[i] = v
}
for i := 0; i <= b.front; i++ {
v, err = fn(ctx, b.window[i], i, i-b.back)
if err != nil {
return err
}
b.window[i] = v
}
return nil
}
2024-03-04 09:21:10 +03:00
// SliceNominal returns a slice into the nominal buffer, using the standard
// [inclusive:exclusive] slicing mechanics.
//
// Boundary checking is relaxed. If the buffer is empty, the returned slice is
// empty. Otherwise, if the requested range is completely outside the bounds of
// the tail window, the returned slice is empty; if the range overlaps with the
// tail window, the returned slice contains the overlapping items. If strict
// boundary checking is important to you, use [Buf.InBounds] to check the start
// and end indices.
//
// SliceNominal is approximately functionality equivalent to reslicing the
// result of [Buf.Tail], but it may avoid wasteful copying (and has relaxed
// bounds checking).
//
// buf := tailbuf.New[int](3).WriteAll(1, 2, 3)
// a := buf.Tail()[0:2]
// b := buf.SliceNominal(0, 2)
// assert.Equal(t, a, b)
//
// If start < 0, zero is used. SliceNominal panics if end is less than start.
func SliceNominal[T any](b *Buf[T], start, end int) []T {
offset := b.Offset()
start -= offset
if start < 0 {
start = 0
}
end -= offset
if end <= start {
return make([]T, 0)
}
return SliceTail(b, start, end)
}
// SliceTail returns a slice of the tail window, using the standard
// [inclusive:exclusive] slicing mechanics, but with permissive bounds checking.
// The slice is freshly allocated, so the caller is free to mutate it.
//
// A call to SliceTail is equivalent to reslicing the result of [Buf.Tail], but
// it may avoid unnecessary copying, depending on the state of Buf.
//
// buf := tailbuf.New[int](3).WriteAll(1, 2, 3)
// a := buf.Tail()[0:2]
// b := buf.SliceTail(0, 2)
// fmt.Println("a:", a, "b:", b)
// // Output: a: [1 2] b: [1 2]
//
// If Buf is empty, the returned slice is empty. Otherwise, if the requested
// range is completely outside the bounds of the tail window, the returned slice
// is empty; if the range overlaps with the tail window, the returned slice
// contains the overlapping items. If strict boundary checking is important, use
// [Buf.InBounds] to check the start and end indices.
//
// SliceTail panics if start is negative or end is less than start.
//
// See also: [SliceNominal], [Buf.Tail], [Buf.Bounds], [Buf.InBounds].
func SliceTail[T any](b *Buf[T], start, end int) []T {
switch {
case start < 0:
panic("start must be >= 0")
case end < start:
panic("end must be >= start")
case len(b.window) == 0, end == start, b.count == 0, start >= b.count:
return make([]T, 0)
case b.count == 1, b.front == b.back:
// Special case: the buffer has only one item.
if start == 0 && end >= 1 {
return []T{b.window[0]}
}
return make([]T, 0)
case b.front > b.back:
if end > b.count {
end = b.count
}
if end > len(b.window) {
end = len(b.window)
}
s := make([]T, 0, end-start)
return append(s, b.window[start:end]...)
default: // b.back > b.front
if end >= b.count {
end = b.count - 1
}
if end > len(b.window) {
end = len(b.window)
}
s := make([]T, 0, end-start)
s = append(s, b.window[b.back+start:]...)
frontIndex := b.front + end - len(b.window) + 1
return append(s, b.window[:frontIndex]...)
}
}