Circular buffer implementation

This commit is contained in:
2024-04-18 17:45:00 +02:00
parent b7244b2bdb
commit 791387fb29
3 changed files with 604 additions and 1 deletions
@@ -0,0 +1,208 @@
using System.Linq;
using System.Threading;
namespace System.Collections.Generic;
public sealed class CircularBuffer<T> : IList<T>
{
private readonly T[] buffer;
private int head;
private int tail;
public int Count { get; private set; }
public bool IsReadOnly { get; } = false;
public T this[int index]
{
get
{
if (index > this.Count)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
return this.buffer[(this.head + index) % this.buffer.Length];
}
set
{
if (index > this.Count)
{
throw new ArgumentOutOfRangeException(nameof(index));
}
this.buffer[(this.head + index) % this.buffer.Length] = value;
}
}
public CircularBuffer(int capacity)
{
if (capacity < 1) throw new ArgumentException($"{nameof(capacity)} cannot be smaller than 1");
this.buffer = new T[capacity];
this.head = 0;
this.tail = 0;
}
public CircularBuffer(T[] buffer, int head, int tail)
{
if (buffer.Length == 0) throw new ArgumentException($"{nameof(buffer)} cannot be of length 0");
this.buffer = buffer;
this.head = head;
this.tail = tail;
this.Count = this.tail < this.head ?
this.tail + this.buffer.Length - this.head + 1 :
this.tail - this.head + 1;
}
public CircularBuffer(T[] buffer)
{
if (buffer.Length == 0) throw new ArgumentException($"{nameof(buffer)} cannot be of length 0");
this.buffer = buffer;
this.head = 0;
this.tail = this.buffer.Length - 1;
this.Count = this.buffer.Length;
}
public void Add(T item)
{
if (this.Count == this.buffer.Length)
{
this.buffer[this.tail] = item;
this.tail = (this.tail + 1) % this.buffer.Length;
this.head = (this.head + 1) % this.buffer.Length;
}
else
{
this.buffer[this.tail] = item;
this.tail = (this.tail + 1) % this.buffer.Length;
this.Count++;
}
}
public void Clear()
{
this.head = 0;
this.tail = 0;
this.Count = 0;
}
public void DeepClear()
{
this.Clear();
Array.Clear(this.buffer, 0, this.buffer.Length);
}
public bool Contains(T item)
{
var comparer = EqualityComparer<T>.Default;
foreach(var itItem in this)
{
if (comparer.Equals(itItem, item))
{
return true;
}
}
return false;
}
public bool Remove(T item)
{
var index = this.head;
var comparer = EqualityComparer<T>.Default;
for (var i = 0; i < this.Count; i++)
{
if (comparer.Equals(this.buffer[index % this.buffer.Length], item))
{
int nextIndex;
for (var j = 0; j < this.Count - i - 1; j++)
{
nextIndex = (index + 1) % this.buffer.Length;
this.buffer[index] = this.buffer[nextIndex];
index = nextIndex;
}
this.Count--;
this.tail = this.tail == 0 ? this.buffer.Length - 1 : this.tail - 1;
return true;
}
index = (index + 1) % this.buffer.Length;
}
return false;
}
public void CopyTo(T[] array, int arrayIndex)
{
foreach(var item in this)
{
array[arrayIndex++] = item;
}
}
public IEnumerator<T> GetEnumerator()
{
var index = this.head;
for (var i = 0; i < this.Count; i++)
{
yield return this.buffer[index];
index = (index + 1) % this.buffer.Length;
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
public int IndexOf(T item)
{
var comparer = EqualityComparer<T>.Default;
for (var i = 0; i < this.Count; i++)
{
if (comparer.Equals(this[i], item))
{
return i;
}
}
return -1;
}
public void Insert(int index, T item)
{
if (index < 0 || index > this.Count) throw new ArgumentOutOfRangeException(nameof(index));
if (this.Count == this.buffer.Length) throw new InvalidOperationException("Cannot insert into a full buffer.");
if (index == this.Count)
{
this.Add(item);
return;
}
for (var i = this.Count; i > index; i--)
{
this.buffer[(this.head + i) % this.buffer.Length] = this.buffer[(this.head + i - 1) % this.buffer.Length];
}
this.buffer[(this.head + index) % this.buffer.Length] = item;
this.tail = (this.tail + 1) % this.buffer.Length;
this.Count++;
}
public void RemoveAt(int index)
{
if (index < 0 || index >= this.Count) throw new ArgumentOutOfRangeException(nameof(index));
for (var i = index; i < this.Count - 1; i++)
{
this.buffer[(this.head + i) % this.buffer.Length] = this.buffer[(this.head + i + 1) % this.buffer.Length];
}
this.tail = (this.tail - 1 + this.buffer.Length) % this.buffer.Length;
this.Count--;
}
}
@@ -7,7 +7,7 @@
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<RootNamespace>System</RootNamespace>
<Version>1.6.2</Version>
<Version>1.6.3</Version>
<Authors>Alexandru Macocian</Authors>
<RepositoryUrl>https://github.com/AlexMacocian/SystemExtensions</RepositoryUrl>
<Description>Extensions for the System namespace</Description>
@@ -0,0 +1,395 @@
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections.Generic;
using System.Linq;
namespace System.Collections.Tests;
[TestClass]
public class CircularBufferTests
{
[TestMethod]
public void Constructor_WithCapacity_RespectsCapacity()
{
var buffer = new CircularBuffer<int>(10);
buffer.Count.Should().Be(0);
for (var i = 0; i < 11; i++)
{
buffer.Add(i);
}
buffer.Count.Should().Be(10);
}
[TestMethod]
public void Constructor_WithBuffer_ReturnsExpectedElements()
{
var array = new int[10];
for(var i = 0; i < 10; i++)
{
array[i] = i;
}
var buffer = new CircularBuffer<int>(array, 0, 9);
buffer.Count.Should().Be(10);
buffer.Should().BeEquivalentTo(array);
}
[TestMethod]
public void Constructor_WithBuffer_RespectsOrder()
{
var expectedArray = new int[] { 3, 4, 5, 6, 7, 8, 9, 0, 1, 2 };
var array = new int[10] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var buffer = new CircularBuffer<int>(array, 3, 2);
buffer.Count.Should().Be(10);
buffer.Should().BeEquivalentTo(expectedArray);
}
[TestMethod]
public void Constructor_WithBuffer_RespectsInternalBuffer()
{
var buffer = new CircularBuffer<int>(new int[] { 0, 1, 2, 3, 4, 5 });
buffer.Count.Should().Be(6);
buffer.Should().BeEquivalentTo(new int[] { 0, 1, 2, 3, 4, 5 });
}
[TestMethod]
public void AddItem_AddsItem()
{
var buffer = new CircularBuffer<int>(10)
{
1
};
buffer.Should().HaveCount(1);
buffer.First().Should().Be(1);
}
[TestMethod]
public void AddItem_AddsItem_RespectsCapacity()
{
var buffer = new CircularBuffer<int>(10)
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10
};
buffer.Add(0);
buffer.Should().HaveCount(10);
buffer.Last().Should().Be(0);
buffer.First().Should().Be(2);
}
[TestMethod]
public void RemoveItem_RemovesDesiredItem()
{
var buffer = new CircularBuffer<int>(10)
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10
};
var result = buffer.Remove(5);
result.Should().BeTrue();
buffer.Count.Should().Be(9);
buffer.Should().BeEquivalentTo(new int[] { 1, 2, 3, 4, 6, 7, 8, 9, 10 });
}
[TestMethod]
public void RemoveItem_UnknownItem_DoesNotRemoveItem()
{
var buffer = new CircularBuffer<int>(10)
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10
};
var result = buffer.Remove(15);
result.Should().BeFalse();
buffer.Count.Should().Be(10);
buffer.Should().BeEquivalentTo(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
}
[TestMethod]
public void Contains_ItemExists_ReturnsTrue()
{
var buffer = new CircularBuffer<int>(10)
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10
};
var result = buffer.Contains(5);
result.Should().BeTrue();
}
[TestMethod]
public void Contains_ItemDoesNotExists_ReturnsFalse()
{
var buffer = new CircularBuffer<int>(10)
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10
};
var result = buffer.Contains(15);
result.Should().BeFalse();
}
[TestMethod]
public void Clear_ClearsBuffer()
{
var buffer = new CircularBuffer<int>(10)
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10
};
buffer.Clear();
buffer.Count.Should().Be(0);
buffer.Should().BeEquivalentTo(Array.Empty<int>());
}
[TestMethod]
public void DeepClear_ClearsBuffer()
{
var buffer = new CircularBuffer<int>(10)
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10
};
buffer.DeepClear();
buffer.Count.Should().Be(0);
buffer.Should().BeEquivalentTo(Array.Empty<int>());
}
[TestMethod]
public void Insert_FullBuffer_ThrowsException()
{
var buffer = new CircularBuffer<int>(10)
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
10
};
var action = () =>
{
buffer.Insert(0, 1);
};
action.Should().Throw<InvalidOperationException>();
}
[TestMethod]
public void Insert_ShouldInsertItemAtPosition()
{
var buffer = new CircularBuffer<int>(10)
{
1,
2,
3,
4,
5,
6,
7,
8,
9,
};
buffer.Insert(0, 0);
buffer.Should().BeEquivalentTo(new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 });
}
[TestMethod]
public void RemoveAt_RemovesExpectedItem()
{
var buffer = new CircularBuffer<int>(10)
{
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
};
buffer.RemoveAt(0);
buffer.Should().BeEquivalentTo(new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
}
[TestMethod]
public void Indexer_ReturnsExpectedItem()
{
var buffer = new CircularBuffer<int>(10)
{
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
};
buffer[5].Should().Be(5);
}
[TestMethod]
public void Indexer_SetsExpectedItem()
{
var buffer = new CircularBuffer<int>(10)
{
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
};
buffer[6] = 5;
buffer.Should().BeEquivalentTo(new int[] { 0, 1, 2, 3, 4, 5, 5, 7, 8, 9 });
}
[TestMethod]
public void CircularBuffer_AddAndRemoveOperations_PerformAsExpected()
{
var buffer = new CircularBuffer<int>(10)
{
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
};
buffer.Add(10);
buffer.Remove(9);
buffer.RemoveAt(5);
buffer.Add(11);
buffer.Should().BeEquivalentTo(new int[] { 1, 2, 3, 4, 5, 7, 8, 10, 11 });
}
[TestMethod]
public void CircularBuffer_AddItems_RotatesBuffer()
{
var buffer = new CircularBuffer<int>(10)
{
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
};
for(var i = 0; i < 10; i++)
{
buffer.Add(i + 10);
}
buffer.Should().BeEquivalentTo(new int[] { 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 });
}
}