diff --git a/SystemExtensions.NetStandard/Collections/CircularBuffer.cs b/SystemExtensions.NetStandard/Collections/CircularBuffer.cs new file mode 100644 index 0000000..f8436bf --- /dev/null +++ b/SystemExtensions.NetStandard/Collections/CircularBuffer.cs @@ -0,0 +1,208 @@ +using System.Linq; +using System.Threading; + +namespace System.Collections.Generic; +public sealed class CircularBuffer : IList +{ + 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.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.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 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.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--; + } +} \ No newline at end of file diff --git a/SystemExtensions.NetStandard/SystemExtensions.NetStandard.csproj b/SystemExtensions.NetStandard/SystemExtensions.NetStandard.csproj index ff943c1..e5f1663 100644 --- a/SystemExtensions.NetStandard/SystemExtensions.NetStandard.csproj +++ b/SystemExtensions.NetStandard/SystemExtensions.NetStandard.csproj @@ -7,7 +7,7 @@ LICENSE true System - 1.6.2 + 1.6.3 Alexandru Macocian https://github.com/AlexMacocian/SystemExtensions Extensions for the System namespace diff --git a/SystemExtensions.Tests/Collections/CircularBufferTests.cs b/SystemExtensions.Tests/Collections/CircularBufferTests.cs new file mode 100644 index 0000000..8224387 --- /dev/null +++ b/SystemExtensions.Tests/Collections/CircularBufferTests.cs @@ -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(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(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(array, 3, 2); + + buffer.Count.Should().Be(10); + buffer.Should().BeEquivalentTo(expectedArray); + } + + [TestMethod] + public void Constructor_WithBuffer_RespectsInternalBuffer() + { + var buffer = new CircularBuffer(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(10) + { + 1 + }; + + buffer.Should().HaveCount(1); + buffer.First().Should().Be(1); + } + + [TestMethod] + public void AddItem_AddsItem_RespectsCapacity() + { + var buffer = new CircularBuffer(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(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(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(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(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(10) + { + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + }; + + buffer.Clear(); + + buffer.Count.Should().Be(0); + buffer.Should().BeEquivalentTo(Array.Empty()); + } + + [TestMethod] + public void DeepClear_ClearsBuffer() + { + var buffer = new CircularBuffer(10) + { + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + }; + + buffer.DeepClear(); + + buffer.Count.Should().Be(0); + buffer.Should().BeEquivalentTo(Array.Empty()); + } + + [TestMethod] + public void Insert_FullBuffer_ThrowsException() + { + var buffer = new CircularBuffer(10) + { + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10 + }; + + var action = () => + { + buffer.Insert(0, 1); + }; + + action.Should().Throw(); + } + + [TestMethod] + public void Insert_ShouldInsertItemAtPosition() + { + var buffer = new CircularBuffer(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(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(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(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(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(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 }); + } +}