From e3f922ef410c683ef28edc4295bb1b83e235d000 Mon Sep 17 00:00:00 2001 From: JCash Date: Wed, 8 Oct 2025 16:52:33 +0200 Subject: [PATCH 1/4] Added ringbuffer::Swap() --- src/jc/cpp/ringbuffer.h | 75 ++++++++++++++++++++++++++++++++++++++++- test/ringbuffer.cpp | 37 ++++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/jc/cpp/ringbuffer.h b/src/jc/cpp/ringbuffer.h index 0fb7072..4330ae5 100644 --- a/src/jc/cpp/ringbuffer.h +++ b/src/jc/cpp/ringbuffer.h @@ -20,6 +20,13 @@ #include #include // disable with NDEBUG +#define JC_SWAP(type, lhs, rhs)\ + {\ + type tmp = rhs;\ + rhs = lhs;\ + lhs = tmp;\ + } + namespace jc { @@ -34,6 +41,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,6 +66,9 @@ 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. @@ -61,10 +79,14 @@ class RingBuffer 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(); // linearizes the ringbuffer into a contiguous array. + void FlattenUnordered(); // faster than Flatten(), good if you are going to sort it directly after anyways + 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; @@ -136,6 +158,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,12 +188,57 @@ T RingBuffer::Pop() return item; } +template +void RingBuffer::Flatten() +{ + uint32_t size = Size(); + + T tmp = m_Buffer[0]; // save this one + + // Copy the items in logical order + for (uint32_t i = 0; i < size; ++i) + { + uint32_t index = (m_Tail + i) % m_Max; + m_Buffer[i] = index ? m_Buffer[index] : tmp; + } + + m_Tail = 0; + m_Head = size-1; +} + +// some cases: .=free, T=Tail, H=Head, *=used +// 1) [T****H....] +// 2) [...T***H..] +// 3) [*H...T****] + +template +void RingBuffer::FlattenUnordered() +{ + if (m_Tail == 0) // case 1, already flattened + return; + + uint32_t size = Size(); + if (m_Tail < m_Head) // case 2: [...T***H..] -> [T***H.....] + { + memmove(m_Buffer, m_Buffer+m_Tail, sizeof(T) * size); + } + else // case 3. Make [*H...T****] -> [*HT****...] -> [T*****H...] + { + memmove(m_Buffer+m_Head, m_Buffer+m_Tail, sizeof(T) * (m_Max - m_Tail)); + } + m_Tail = 0; + m_Head = size-1; +} + } // namespace +#undef JC_SWAP + /* VERSION: + 1.1 Added Swap() function 1.0 Initial version diff --git a/test/ringbuffer.cpp b/test/ringbuffer.cpp index 59ab1d7..be5466b 100644 --- a/test/ringbuffer.cpp +++ b/test/ringbuffer.cpp @@ -203,3 +203,40 @@ 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]); +} From e88809ad6c3ce968b505ae3c0dff8d7b656117b4 Mon Sep 17 00:00:00 2001 From: JCash Date: Wed, 8 Oct 2025 19:31:36 +0200 Subject: [PATCH 2/4] Added support for Flatten() and FlattenUnsorted() --- src/jc/cpp/ringbuffer.h | 92 +++++++++++++++------ test/ringbuffer.cpp | 177 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 240 insertions(+), 29 deletions(-) diff --git a/src/jc/cpp/ringbuffer.h b/src/jc/cpp/ringbuffer.h index 4330ae5..0d2cb2c 100644 --- a/src/jc/cpp/ringbuffer.h +++ b/src/jc/cpp/ringbuffer.h @@ -20,6 +20,8 @@ #include #include // disable with NDEBUG +#include // memmove + #define JC_SWAP(type, lhs, rhs)\ {\ type tmp = rhs;\ @@ -80,8 +82,8 @@ class RingBuffer const T& operator[] (size_t i) const { assert(i < Size()); return m_Buffer[(m_Tail + i) % m_Max]; } // Useful for sorting - void Flatten(); // linearizes the ringbuffer into a contiguous array. - void FlattenUnordered(); // faster than Flatten(), good if you are going to sort it directly after anyways + 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 @@ -100,6 +102,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; @@ -188,48 +192,90 @@ T RingBuffer::Pop() return item; } + +// some cases: .=free, T=Tail, H=Head, *=used +// 1) [T****H....] +// 2) [...T***H..] +// 3) [*H...T****] + template -void RingBuffer::Flatten() +void RingBuffer::FlattenUnordered() { + uint32_t capacity = m_Max; uint32_t size = Size(); + if (size == 0 || m_Tail == 0) + return; // already contiguous or empty - T tmp = m_Buffer[0]; // save this one - - // Copy the items in logical order - for (uint32_t i = 0; i < size; ++i) + 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 { - uint32_t index = (m_Tail + i) % m_Max; - m_Buffer[i] = index ? m_Buffer[index] : tmp; + // 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-1; + m_Head = size; } -// some cases: .=free, T=Tail, H=Head, *=used -// 1) [T****H....] -// 2) [...T***H..] -// 3) [*H...T****] - +// 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::FlattenUnordered() +void RingBuffer::Flatten() { - if (m_Tail == 0) // case 1, already flattened + 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; - uint32_t size = Size(); - if (m_Tail < m_Head) // case 2: [...T***H..] -> [T***H.....] + if (m_Tail < m_Head) { - memmove(m_Buffer, m_Buffer+m_Tail, sizeof(T) * size); + // Single contiguous block but offset; shift down to index 0 + memmove(m_Buffer, m_Buffer + m_Tail, sizeof(T) * size); } - else // case 3. Make [*H...T****] -> [*HT****...] -> [T*****H...] + else { - memmove(m_Buffer+m_Head, m_Buffer+m_Tail, sizeof(T) * (m_Max - m_Tail)); + // 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-1; + 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 diff --git a/test/ringbuffer.cpp b/test/ringbuffer.cpp index be5466b..1361a24 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"); } @@ -240,3 +241,167 @@ TEST(RingBufferTest, Swap) 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); +} From c58e85be7a00ee15be7fe7e2104713cfe589cb33 Mon Sep 17 00:00:00 2001 From: JCash Date: Wed, 8 Oct 2025 23:25:43 +0200 Subject: [PATCH 3/4] Added support for Erase() function --- src/jc/cpp/ringbuffer.h | 35 +++++++++++++++++++++++++++ test/ringbuffer.cpp | 52 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/src/jc/cpp/ringbuffer.h b/src/jc/cpp/ringbuffer.h index 0d2cb2c..2367133 100644 --- a/src/jc/cpp/ringbuffer.h +++ b/src/jc/cpp/ringbuffer.h @@ -77,6 +77,8 @@ class RingBuffer 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]; } @@ -192,6 +194,39 @@ 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....] diff --git a/test/ringbuffer.cpp b/test/ringbuffer.cpp index 1361a24..8f57682 100644 --- a/test/ringbuffer.cpp +++ b/test/ringbuffer.cpp @@ -405,3 +405,55 @@ TEST(RingBufferTest, FlattenUnordered_Wrapped) 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]); + } +} From 7aabdf52316485937c23648adae415cc0cf7dd86 Mon Sep 17 00:00:00 2001 From: JCash Date: Wed, 8 Oct 2025 23:26:47 +0200 Subject: [PATCH 4/4] added version number --- src/jc/cpp/ringbuffer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jc/cpp/ringbuffer.h b/src/jc/cpp/ringbuffer.h index 2367133..fd6e287 100644 --- a/src/jc/cpp/ringbuffer.h +++ b/src/jc/cpp/ringbuffer.h @@ -319,8 +319,8 @@ void RingBuffer::ReverseRange(uint32_t begin, uint32_t end_exclusive) VERSION: - 1.1 Added Swap() function - 1.0 Initial version + 1.1 2025-10-08 Added Swap(), Erase() and Flatten*() functions + 1.0 Initial version LICENSE: