using System; using System.Collections; using System.Collections.Generic; namespace HurricaneVR.Framework.Shared.Utilities { /// /// /// Circular buffer. /// /// When writing to a full buffer: /// PushBack -> removes this[0] / Front() /// PushFront -> removes this[Size-1] / Back() /// /// this implementation is inspired by /// http://www.boost.org/doc/libs/1_53_0/libs/circular_buffer/doc/circular_buffer.html /// because I liked their interface. /// public class CircularBuffer : IEnumerable { private readonly T[] _buffer; /// /// The _start. Index of the first element in buffer. /// private int _start; /// /// The _end. Index after the last element in the buffer. /// private int _end; /// /// The _size. Buffer size. /// private int _size; public CircularBuffer(int capacity) : this(capacity, new T[] { }) { } /// /// Initializes a new instance of the class. /// /// /// /// Buffer capacity. Must be positive. /// /// /// Items to fill buffer with. Items length must be less than capacity. /// Suggestion: use Skip(x).Take(y).ToArray() to build this argument from /// any enumerable. /// public CircularBuffer(int capacity, T[] items) { if (capacity < 1) { throw new ArgumentException( "Circular buffer cannot have negative or zero capacity.", nameof(capacity)); } if (items == null) { throw new ArgumentNullException(nameof(items)); } if (items.Length > capacity) { throw new ArgumentException( "Too many items to fit circular buffer", nameof(items)); } _buffer = new T[capacity]; Array.Copy(items, _buffer, items.Length); _size = items.Length; _start = 0; _end = _size == capacity ? 0 : _size; } /// /// Maximum capacity of the buffer. Elements pushed into the buffer after /// maximum capacity is reached (IsFull = true), will remove an element. /// public int Capacity { get { return _buffer.Length; } } public bool IsFull { get { return Size == Capacity; } } public bool IsEmpty { get { return Size == 0; } } /// /// Current buffer size (the number of elements that the buffer has). /// public int Size { get { return _size; } } /// /// Element at the front of the buffer - this[0]. /// /// The value of the element of type T at the front of the buffer. public T Front() { ThrowIfEmpty(); return _buffer[_start]; } /// /// Element at the back of the buffer - this[Size - 1]. /// /// The value of the element of type T at the back of the buffer. public T Back() { ThrowIfEmpty(); return _buffer[(_end != 0 ? _end : Capacity) - 1]; } public T this[int index] { get { if (IsEmpty) { throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index)); } if (index >= _size) { throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size)); } int actualIndex = InternalIndex(index); return _buffer[actualIndex]; } set { if (IsEmpty) { throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer is empty", index)); } if (index >= _size) { throw new IndexOutOfRangeException(string.Format("Cannot access index {0}. Buffer size is {1}", index, _size)); } int actualIndex = InternalIndex(index); _buffer[actualIndex] = value; } } /// /// Pushes a new element to the back of the buffer. Back()/this[Size-1] /// will now return this element. /// /// When the buffer is full, the element at Front()/this[0] will be /// popped to allow for this new element to fit. /// /// Item to push to the back of the buffer public void Dequeue(T item) { if (IsFull) { _buffer[_end] = item; Increment(ref _end); _start = _end; } else { _buffer[_end] = item; Increment(ref _end); ++_size; } } /// /// Pushes a new element to the front of the buffer. Front()/this[0] /// will now return this element. /// /// When the buffer is full, the element at Back()/this[Size-1] will be /// popped to allow for this new element to fit. /// /// Item to push to the front of the buffer public void Enqueue(T item) { if (IsFull) { Decrement(ref _start); _end = _start; _buffer[_start] = item; } else { Decrement(ref _start); _buffer[_start] = item; ++_size; } } /// /// Removes the element at the back of the buffer. Decreasing the /// Buffer size by 1. /// public void PopBack() { ThrowIfEmpty("Cannot take elements from an empty buffer."); Decrement(ref _end); _buffer[_end] = default(T); --_size; } /// /// Removes the element at the front of the buffer. Decreasing the /// Buffer size by 1. /// public void PopFront() { ThrowIfEmpty("Cannot take elements from an empty buffer."); _buffer[_start] = default(T); Increment(ref _start); --_size; } /// /// Copies the buffer contents to an array, according to the logical /// contents of the buffer (i.e. independent of the internal /// order/contents) /// /// A new array with a copy of the buffer contents. public T[] ToArray() { T[] newArray = new T[Size]; int newArrayOffset = 0; var segments = new ArraySegment[2] { ArrayOne(), ArrayTwo() }; foreach (ArraySegment segment in segments) { Array.Copy(segment.Array, segment.Offset, newArray, newArrayOffset, segment.Count); newArrayOffset += segment.Count; } return newArray; } #region IEnumerable implementation public IEnumerator GetEnumerator() { var segments = new ArraySegment[2] { ArrayOne(), ArrayTwo() }; foreach (ArraySegment segment in segments) { for (int i = 0; i < segment.Count; i++) { yield return segment.Array[segment.Offset + i]; } } } #endregion #region IEnumerable implementation IEnumerator IEnumerable.GetEnumerator() { return (IEnumerator)GetEnumerator(); } #endregion private void ThrowIfEmpty(string message = "Cannot access an empty buffer.") { if (IsEmpty) { throw new InvalidOperationException(message); } } /// /// Increments the provided index variable by one, wrapping /// around if necessary. /// /// private void Increment(ref int index) { if (++index == Capacity) { index = 0; } } /// /// Decrements the provided index variable by one, wrapping /// around if necessary. /// /// private void Decrement(ref int index) { if (index == 0) { index = Capacity; } index--; } /// /// Converts the index in the argument to an index in _buffer /// /// /// The transformed index. /// /// /// External index. /// private int InternalIndex(int index) { return _start + (index < (Capacity - _start) ? index : index - Capacity); } // doing ArrayOne and ArrayTwo methods returning ArraySegment as seen here: // http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1957cccdcb0c4ef7d80a34a990065818d // http://www.boost.org/doc/libs/1_37_0/libs/circular_buffer/doc/circular_buffer.html#classboost_1_1circular__buffer_1f5081a54afbc2dfc1a7fb20329df7d5b // should help a lot with the code. #region Array items easy access. // The array is composed by at most two non-contiguous segments, // the next two methods allow easy access to those. private ArraySegment ArrayOne() { if (IsEmpty) { return new ArraySegment(new T[0]); } else if (_start < _end) { return new ArraySegment(_buffer, _start, _end - _start); } else { return new ArraySegment(_buffer, _start, _buffer.Length - _start); } } private ArraySegment ArrayTwo() { if (IsEmpty) { return new ArraySegment(new T[0]); } else if (_start < _end) { return new ArraySegment(_buffer, _end, 0); } else { return new ArraySegment(_buffer, 0, _end); } } #endregion } }