using System.Collections; namespace Sandbox.Utility; // From https://github.com/joaoportela/CircularBuffer-CSharp ( no license ) /// /// Circular buffer, push pop and index access is always O(1). /// 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; /// /// Initializes a new instance of the class. /// /// /// /// Buffer capacity. Must be positive. /// 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; } } /// /// Boolean indicating if Circular is at full capacity. /// Adding more elements when the buffer is full will /// cause elements to be removed from the other end /// of the buffer. /// public bool IsFull { get { return Size == Capacity; } } /// /// True if has no elements. /// 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]; } /// /// Index access to elements in buffer. /// Index does not loop around like when adding elements, /// valid interval is [0;Size[ /// /// Index of element to access. /// Thrown when index is outside of [; Size[ interval. 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 PushBack( 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 PushFront( 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; } /// /// Clears the contents of the array. Size = 0, Capacity is unchanged. /// /// public void Clear() { // to clear we just reset everything. _start = 0; _end = 0; _size = 0; Array.Clear( _buffer, 0, _buffer.Length ); } /// /// 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; foreach ( ArraySegment segment in ToArraySegments() ) { Array.Copy( segment.Array, segment.Offset, newArray, newArrayOffset, segment.Count ); newArrayOffset += segment.Count; } return newArray; } /// /// Get the contents of the buffer as 2 ArraySegments. /// Respects the logical contents of the buffer, where /// each segment and items in each segment are ordered /// according to insertion. /// /// Fast: does not copy the array elements. /// Useful for methods like Send(IList<ArraySegment<Byte>>). /// /// Segments may be empty. /// /// An IList with 2 segments corresponding to the buffer content. public IEnumerable> ToArraySegments() { yield return ArrayOne(); yield return ArrayTwo(); } #region IEnumerable implementation /// /// Returns an enumerator that iterates through this buffer. /// /// An enumerator that can be used to iterate this collection. public IEnumerator GetEnumerator() { foreach ( ArraySegment segment in ToArraySegments() ) { 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( Array.Empty() ); } 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( Array.Empty() ); } else if ( _start < _end ) { return new ArraySegment( _buffer, _end, 0 ); } else { return new ArraySegment( _buffer, 0, _end ); } } #endregion }