diff --git a/src/jc/cpp/ringbuffer.h b/src/jc/cpp/ringbuffer.h index 0fb7072..fd6e287 100644 --- a/src/jc/cpp/ringbuffer.h +++ b/src/jc/cpp/ringbuffer.h @@ -20,6 +20,15 @@ #include #include // disable with NDEBUG +#include // memmove + +#define JC_SWAP(type, lhs, rhs)\ + {\ + type tmp = rhs;\ + rhs = lhs;\ + lhs = tmp;\ + } + namespace jc { @@ -34,6 +43,14 @@ class RingBuffer RingBuffer(uint32_t capacity) : m_Buffer(0), m_Head(0), m_Tail(0), m_Max(0) { SetCapacity(capacity); } ~RingBuffer() { Free(); } + void Swap(RingBuffer& rhs) { + JC_SWAP(T*, m_Buffer, rhs.m_Buffer); + JC_SWAP(uint32_t, m_Head, rhs.m_Head); + JC_SWAP(uint32_t, m_Tail, rhs.m_Tail); + JC_SWAP(uint32_t, m_Max, rhs.m_Max); + JC_SWAP(uint32_t, m_Full, rhs.m_Full); + } + /// Gets the size of the buffer uint32_t Size() const { if (m_Full) @@ -51,20 +68,29 @@ class RingBuffer uint32_t Capacity() const { return (uint32_t)m_Max; } /// Increases or decreases capacity. Invalidates pointers to elements void SetCapacity(uint32_t capacity); + /// Increases or decreases capacity. Invalidates pointers to elements + void OffsetCapacity(uint32_t grow); + /// Adds one item to the ring buffer. Asserts if the buffer is full void Push(const T& item); /// Adds one item to the ring buffer. Does not assert, If full, overwrites the value at the tail. void PushUnchecked(const T& item); /// Removes (and returns) the first inserted item from the ring buffer. Asserts if the buffer is empty T Pop(); + /// Removes the element at logical index (0-based) and keeps order + void Erase(uint32_t index); T& operator[] (size_t i) { assert(i < Size()); return m_Buffer[(m_Tail + i) % m_Max]; } const T& operator[] (size_t i) const { assert(i < Size()); return m_Buffer[(m_Tail + i) % m_Max]; } + // Useful for sorting + void Flatten(); // in-place linearization so data starts at m_Buffer[0] + void FlattenUnordered(); // compacts contiguously without preserving logical order + T* Buffer() const { return m_Buffer; } + // Mainly for unit tests uint32_t Head() const { return m_Head; } uint32_t Tail() const { return m_Tail; } - T* Buffer() const { return m_Buffer; } private: T* m_Buffer; @@ -78,6 +104,8 @@ class RingBuffer RingBuffer& operator= (const RingBuffer& rhs); bool operator== (const RingBuffer& rhs); + void ReverseRange(uint32_t begin, uint32_t end_exclusive); + void Free() { free(m_Buffer); m_Buffer = 0; @@ -136,6 +164,12 @@ void RingBuffer::SetCapacity(uint32_t capacity) m_Full = capacity == size ? 1 : 0; } +template +void RingBuffer::OffsetCapacity(uint32_t grow) +{ + SetCapacity(Capacity() + grow); +} + template void RingBuffer::Push(const T& item) { @@ -160,13 +194,133 @@ T RingBuffer::Pop() return item; } +template +void RingBuffer::Erase(uint32_t index) +{ + uint32_t size = Size(); + assert(index < size); + + // Compute physical index of the element to erase + uint32_t pos = (m_Tail + index) % m_Max; + + // Shift elements left from pos towards head to preserve order + // Stop when the next index equals m_Head (the logical end) + while (true) + { + uint32_t next = pos + 1; + if (next >= m_Max) next = 0; + if (next == m_Head) + break; + m_Buffer[pos] = m_Buffer[next]; + pos = next; + } + + // One element removed: move head back by one and clear full flag + if (m_Head == 0) + { + m_Head = m_Max - 1; + } + else + { + --m_Head; + } + m_Full = 0; +} + + +// some cases: .=free, T=Tail, H=Head, *=used +// 1) [T****H....] +// 2) [...T***H..] +// 3) [*H...T****] + +template +void RingBuffer::FlattenUnordered() +{ + uint32_t capacity = m_Max; + uint32_t size = Size(); + if (size == 0 || m_Tail == 0) + return; // already contiguous or empty + + if (m_Tail < m_Head) + { + // Single contiguous block at offset; shift entire block to start + memmove(m_Buffer, m_Buffer + m_Tail, sizeof(T) * size); + } + else + { + // Wrapped case: move the rightmost part down directly after the left part. + // This compacts elements into [0..size-1] without preserving order. + uint32_t right_len = capacity - m_Tail; // [m_Tail .. m_Max) + memmove(m_Buffer + m_Head, m_Buffer + m_Tail, sizeof(T) * right_len); + } + + m_Tail = 0; + m_Head = size; +} + +// In-place linearization. After this call, the logical sequence is contiguous +// at m_Buffer[0..Size()-1]. Does not allocate temporary buffers. +template +void RingBuffer::Flatten() +{ + uint32_t capacity = m_Max; + uint32_t size = Size(); + + // Early out when already contiguous (tail at 0) or empty + // No state changes in these cases. + if (size == 0 || m_Tail == 0) + return; + + if (m_Tail < m_Head) + { + // Single contiguous block but offset; shift down to index 0 + memmove(m_Buffer, m_Buffer + m_Tail, sizeof(T) * size); + } + else + { + // Wrapped case: rotate the whole buffer left by m_Tail positions. + // This makes the logical tail land at index 0. Elements outside + // [0..size-1] are unspecified and may contain moved free slots. + uint32_t n = capacity; + uint32_t k = m_Tail % n; + if (k) + { + ReverseRange(0, k); + ReverseRange(k, n); + ReverseRange(0, n); + } + } + + m_Tail = 0; + m_Head = size; +} + +template +void RingBuffer::ReverseRange(uint32_t begin, uint32_t end_exclusive) +{ + while (begin < end_exclusive) + { + --end_exclusive; + if (begin >= end_exclusive) + break; + T tmp = m_Buffer[begin]; + m_Buffer[begin] = m_Buffer[end_exclusive]; + m_Buffer[end_exclusive] = tmp; + ++begin; + } +} + + } // namespace +#undef JC_SWAP + /* VERSION: - 1.0 Initial version + 1.1 2025-10-08 Added Swap(), Erase() and Flatten*() functions + 1.0 Initial version LICENSE: diff --git a/test/ringbuffer.cpp b/test/ringbuffer.cpp index 59ab1d7..8f57682 100644 --- a/test/ringbuffer.cpp +++ b/test/ringbuffer.cpp @@ -5,7 +5,8 @@ #define JC_TEST_USE_DEFAULT_MAIN #include "jc_test.h" -static void DebugPrint(jc::RingBuffer& buf) { +static void DebugPrint(jc::RingBuffer& buf) +{ uint32_t cap = buf.Capacity(); uint32_t tail = buf.Tail(); uint32_t head = buf.Head(); @@ -15,20 +16,20 @@ static void DebugPrint(jc::RingBuffer& buf) { printf(" "); for (uint32_t i = 0; i < cap; ++i) { - printf("%d", data[i]); + printf("%2d", data[i]); } printf("\n"); printf(" "); for (uint32_t i = 0; i < cap; ++i) { if (head == i && head == tail) - printf("^"); + printf(" ^"); else if (head == i) - printf("H"); + printf(" H"); else if (tail == i) - printf("T"); + printf(" T"); else - printf(" "); + printf(" "); } printf("\n"); } @@ -203,3 +204,256 @@ TEST(RingBufferTest, Add) ASSERT_TRUE(a.Empty()); ASSERT_FALSE(a.Full()); } + +TEST(RingBufferTest, Swap) +{ + jc::RingBuffer a(5); + jc::RingBuffer b(3); + + a.Push(1); + a.Push(2); + a.Push(3); + + b.Push(4); + b.Push(5); + + ASSERT_EQ(5, a.Capacity()); + ASSERT_EQ(3, a.Size()); + ASSERT_EQ(1, a[0]); + ASSERT_EQ(2, a[1]); + ASSERT_EQ(3, a[2]); + + ASSERT_EQ(3, b.Capacity()); + ASSERT_EQ(2, b.Size()); + ASSERT_EQ(4, b[0]); + ASSERT_EQ(5, b[1]); + + a.Swap(b); + + ASSERT_EQ(5, b.Capacity()); + ASSERT_EQ(3, b.Size()); + ASSERT_EQ(1, b[0]); + ASSERT_EQ(2, b[1]); + ASSERT_EQ(3, b[2]); + + ASSERT_EQ(3, a.Capacity()); + ASSERT_EQ(2, a.Size()); + ASSERT_EQ(4, a[0]); + ASSERT_EQ(5, a[1]); +} + +TEST(RingBufferTest, Flatten_InPlace_ContiguousOffset) +{ + jc::RingBuffer rb(10); + // Push 6 elements: 0..5 + for (int i = 0; i < 6; ++i) rb.Push(i); + // Pop 2 -> logical sequence 2..5, contiguous but offset (tail=2) + rb.Pop(); rb.Pop(); + + ASSERT_EQ(4u, rb.Size()); + rb.Flatten(); + + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(4u, rb.Head()); + const int expected1[4] = {2,3,4,5}; + ASSERT_ARRAY_EQ_LEN(expected1, rb.Buffer(), 4); +} + +TEST(RingBufferTest, Flatten_InPlace_Wrapped) +{ + jc::RingBuffer rb(10); + // Fill to full 0..9 + for (int i = 0; i < 10; ++i) rb.Push(i); + // Pop 6 -> tail moves to 6, size=4 + for (int i = 0; i < 6; ++i) rb.Pop(); + // Push 4 more -> wrap head, logical seq 6..13, size=8 + for (int i = 10; i < 14; ++i) rb.Push(i); + + ASSERT_EQ(8u, rb.Size()); + rb.Flatten(); + + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(8u, rb.Head()); + const int expected2[8] = {6,7,8,9,10,11,12,13}; + ASSERT_ARRAY_EQ_LEN(expected2, rb.Buffer(), 8); +} + +TEST(RingBufferTest, Flatten_HeadZero_TailPositive) +{ + jc::RingBuffer rb(10); + // Push 6 elements: 0..5 => head=6, tail=0 + for (int i = 0; i < 6; ++i) rb.Push(i); + // Pop 4 => head=6, tail=4, size=2 + for (int i = 0; i < 4; ++i) rb.Pop(); + // Push 4 elements: 6..9 => head wraps to 0, tail=4, size=6, not full + for (int i = 6; i < 10; ++i) rb.Push(i); + + ASSERT_EQ(0u, rb.Head()); + ASSERT_EQ(4u, rb.Tail()); + ASSERT_EQ(6u, rb.Size()); + ASSERT_FALSE(rb.Full()); + + rb.Flatten(); + + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(6u, rb.Head()); + const int expected[6] = {4,5,6,7,8,9}; + ASSERT_ARRAY_EQ_LEN(expected, rb.Buffer(), 6); +} + +TEST(RingBufferTest, Flatten_AlreadyContiguous_Empty) +{ + jc::RingBuffer rb(10); + ASSERT_EQ(0u, rb.Size()); + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(0u, rb.Head()); + ASSERT_FALSE(rb.Full()); + + rb.Flatten(); + + ASSERT_EQ(0u, rb.Size()); + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(0u, rb.Head()); + ASSERT_FALSE(rb.Full()); +} + +TEST(RingBufferTest, Flatten_AlreadyContiguous_TailZero_NotFull) +{ + jc::RingBuffer rb(10); + for (int i = 0; i < 6; ++i) rb.Push(i); + + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(6u, rb.Head()); + ASSERT_EQ(6u, rb.Size()); + ASSERT_FALSE(rb.Full()); + + rb.Flatten(); + + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(6u, rb.Head()); + const int expected[6] = {0,1,2,3,4,5}; + ASSERT_ARRAY_EQ_LEN(expected, rb.Buffer(), 6); +} + +TEST(RingBufferTest, Flatten_AlreadyContiguous_TailZero_Full) +{ + jc::RingBuffer rb(10); + for (int i = 0; i < 10; ++i) rb.Push(i); + ASSERT_TRUE(rb.Full()); + ASSERT_EQ(10u, rb.Size()); + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(0u, rb.Head()); + + rb.Flatten(); + + ASSERT_TRUE(rb.Full()); + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(0u, rb.Head()); + const int expected[10] = {0,1,2,3,4,5,6,7,8,9}; + ASSERT_ARRAY_EQ_LEN(expected, rb.Buffer(), 10); +} + +// Removed AssertSetEquals; use ASSERT_ARRAY_EQ_LEN to enforce physical start at index 0 + +TEST(RingBufferTest, FlattenUnordered_Contiguous_NoOp) +{ + jc::RingBuffer rb(8); + for (int i = 0; i < 5; ++i) rb.Push(i); + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(5u, rb.Head()); + const int before[5] = {0,1,2,3,4}; + ASSERT_ARRAY_EQ_LEN(before, rb.Buffer(), 5); + + rb.FlattenUnordered(); + + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(5u, rb.Head()); + ASSERT_ARRAY_EQ_LEN(before, rb.Buffer(), 5); +} + +TEST(RingBufferTest, FlattenUnordered_ContiguousOffset) +{ + jc::RingBuffer rb(8); + for (int i = 0; i < 5; ++i) rb.Push(i); // 0..4 + rb.Pop(); rb.Pop(); // logical: 2..4 + // make it contiguous offset: tail=2, head=5 + ASSERT_EQ(2u, rb.Tail()); + ASSERT_EQ(5u, rb.Head()); + rb.FlattenUnordered(); + + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(3u, rb.Head()); + const int expected[3] = {2,3,4}; + ASSERT_ARRAY_EQ_LEN(expected, rb.Buffer(), 3); +} + +TEST(RingBufferTest, FlattenUnordered_Wrapped) +{ + jc::RingBuffer rb(8); + for (int i = 0; i < 8; ++i) rb.Push(i); // full + for (int i = 0; i < 5; ++i) rb.Pop(); // size=3, tail moves to 5 + for (int i = 8; i < 12; ++i) rb.Push(i); // wrap, size=7, tail=5, head=4 + + ASSERT_EQ(5u, rb.Tail()); + ASSERT_EQ(4u, rb.Head()); + ASSERT_EQ(7u, rb.Size()); + + rb.FlattenUnordered(); + + ASSERT_EQ(0u, rb.Tail()); + ASSERT_EQ(7u, rb.Head()); + const int expected[7] = {8,9,10,11,5,6,7}; + ASSERT_ARRAY_EQ_LEN(expected, rb.Buffer(), 7); +} + +TEST(RingBufferTest, Erase_Contiguous_Middle) +{ + jc::RingBuffer rb(8); + for (int i = 0; i < 6; ++i) rb.Push(i); // 0..5 + + rb.Erase(2); // remove '2' + ASSERT_EQ(5u, rb.Size()); + const int expected1[5] = {0,1,3,4,5}; + for (uint32_t i = 0; i < 5; ++i) + { + ASSERT_EQ(expected1[i], rb[i]); + } +} + +TEST(RingBufferTest, Erase_Wrapped_Middle) +{ + jc::RingBuffer rb(8); + for (int i = 0; i < 8; ++i) rb.Push(i); // 0..7 full + for (int i = 0; i < 5; ++i) rb.Pop(); // -> [5,6,7] + for (int i = 8; i < 12; ++i) rb.Push(i); // wrap -> [5,6,7,8,9,10,11] + + rb.Erase(3); // remove '8' + + ASSERT_EQ(6u, rb.Size()); + const int expected2[6] = {5,6,7,9,10,11}; + for (uint32_t i = 0; i < 6; ++i) + { + ASSERT_EQ(expected2[i], rb[i]); + } +} + +TEST(RingBufferTest, Erase_HeadZero_TailPositive) +{ + jc::RingBuffer rb(10); + // Create head==0, tail>0, sequence [4,5,6,7,8,9] + for (int i = 0; i < 6; ++i) rb.Push(i); // 0..5 + for (int i = 0; i < 4; ++i) rb.Pop(); // tail=4, size=2 + for (int i = 6; i < 10; ++i) rb.Push(i); // wraps, head==0 + + ASSERT_EQ(0u, rb.Head()); + ASSERT_EQ(4u, rb.Tail()); + + rb.Erase(1); // remove '5' + + ASSERT_EQ(5u, rb.Size()); + const int expected3[5] = {4,6,7,8,9}; + for (uint32_t i = 0; i < 5; ++i) + { + ASSERT_EQ(expected3[i], rb[i]); + } +}