diff --git a/ring/ring.go b/ring/ring.go new file mode 100755 index 0000000..41390ec --- /dev/null +++ b/ring/ring.go @@ -0,0 +1,95 @@ +package ring + +import "golang.org/x/exp/constraints" + +type Buffer[T any] struct { + Data []T + Start int64 + Len int64 + Cap int64 +} + +func (b *Buffer[T]) Append(x ...T) { + + inLen := int64(len(x)) + + for len(x) > 0 { + + copied := copy(b.Data[b.Head():], x) + x = x[copied:] + + if b.Len == b.Cap { + b.Start = (b.Start + int64(copied)) % (b.Cap) + } else { + b.Len = clamp(b.Len+inLen, 0, b.Cap) + } + } +} + +func (b *Buffer[T]) Head() int64 { + return (b.Start + b.Len) % b.Cap +} + +func (b *Buffer[T]) Insert(index uint64, x ...T) { + + delta := int64(len(x)) + newLen := b.Len + delta + if newLen <= b.Cap { + + copy(b.Data[b.Start+int64(index)+delta:], b.Data[index:]) + copy(b.Data[index:], x) + + b.Len = newLen + return + } +} + +func (b *Buffer[T]) Delete(delStartIndex, elementsToDel uint64, x ...T) { + + if b.Len == 0 { + return + } + + copy(b.Data[uint64(b.Start)+delStartIndex:], b.Data[uint64(b.Start)+delStartIndex+elementsToDel:]) + + // p.cmdBufLen-- +} + +func clamp[T constraints.Ordered](x, min, max T) T { + + if x < min { + return min + } + + if x > max { + return max + } + + return x +} + +// Views returns two slices that have 'Len' elements in total between them. +// The first slice is from Start till min(Start+Len, Cap). If Start+Len<=Cap then the first slice contains all the data and the second is nil. +// If Start+Len>Cap then the first slice contains the data from Start till Cap, and the second slice contains data from Zero till Start+Len-Cap (basically the remaining elements to reach Len in total) +// +// This function does NOT copy. Any changes on the returned slices will reflect on the buffer Data +func (b *Buffer[T]) Views() (v1, v2 []T) { + + if b.Start+b.Len <= b.Cap { + return b.Data[b.Start : b.Start+b.Len], nil + } + + v1 = b.Data[b.Start:b.Cap] + v2 = b.Data[:b.Start+b.Len-b.Cap] + return +} + +func NewBuffer[T any](capacity uint64) *Buffer[T] { + + return &Buffer[T]{ + Data: make([]T, capacity), + Start: 0, + Len: 0, + Cap: int64(capacity), + } +} diff --git a/ring/ring_test.go b/ring/ring_test.go new file mode 100755 index 0000000..755c1c8 --- /dev/null +++ b/ring/ring_test.go @@ -0,0 +1,80 @@ +package ring_test + +import ( + "testing" + + "github.com/bloeys/nterm/ring" +) + +func TestRing(t *testing.T) { + + // Basics + b := ring.NewBuffer[rune](4) + b.Append('a', 'b', 'c', 'd') + CheckArr(t, []rune{'a', 'b', 'c', 'd'}, b.Data) + + v1, v2 := b.Views() + CheckArr(t, []rune{'a', 'b', 'c', 'd'}, v1) + CheckArr(t, nil, v2) + Check(t, 0, b.Start) + Check(t, 4, b.Len) + + b.Append('e', 'f') + Check(t, 2, b.Start) + CheckArr(t, []rune{'e', 'f', 'c', 'd'}, b.Data) + + v1, v2 = b.Views() + CheckArr(t, []rune{'c', 'd'}, v1) + CheckArr(t, []rune{'e', 'f'}, v2) + + b.Append('g') + Check(t, 3, b.Start) + + v1, v2 = b.Views() + CheckArr(t, []rune{'e', 'f', 'g', 'd'}, b.Data) + CheckArr(t, []rune{'d'}, v1) + CheckArr(t, []rune{'e', 'f', 'g'}, v2) + + // Input over 2x bigger than buffer + b = ring.NewBuffer[rune](4) + b.Append('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i') + CheckArr(t, []rune{'i', 'f', 'g', 'h'}, b.Data) + + // Input starting in the middle and having to loop back + b2 := ring.NewBuffer[int](4) + b2.Append(1, 2, 3) + + b2.Append(4, 5) + CheckArr(t, []int{5, 2, 3, 4}, b2.Data) + + b2.Append(6) + CheckArr(t, []int{5, 6, 3, 4}, b2.Data) + + b2.Append(7) + CheckArr(t, []int{5, 6, 7, 4}, b2.Data) + + b2.Append(8) + CheckArr(t, []int{5, 6, 7, 8}, b2.Data) +} + +func Check[T comparable](t *testing.T, expected, got T) { + if got != expected { + t.Fatalf("Expected %v but got %v\n", expected, got) + } +} + +func CheckArr[T comparable](t *testing.T, expected, got []T) { + + if len(expected) != len(got) { + t.Fatalf("Expected %v but got %v\n", expected, got) + return + } + + for i := 0; i < len(expected); i++ { + + if expected[i] != got[i] { + t.Fatalf("Expected %v but got %v\n", expected, got) + return + } + } +}