mirror of
https://github.com/neilotoole/sq.git
synced 2024-12-20 06:31:32 +03:00
6b0918094d
* More diff
364 lines
10 KiB
Go
364 lines
10 KiB
Go
// 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
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
// 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
|
|
}
|
|
|
|
// 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]...)
|
|
}
|
|
}
|