Files

229 lines
5.6 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
namespace FinlyticTechnicals.Timeframe;
/// <summary>
/// Thread-safe, high-performance circular ring buffer with zero allocations on updates.
/// Holds a fixed capacity of items (e.g. 500 candles).
/// </summary>
/// <typeparam name="T">The item type (e.g. CandleDto)</typeparam>
public class CircularRingBuffer<T> : IReadOnlyList<T>
{
private readonly T[] _buffer;
private readonly int _capacity;
private int _start;
private int _count;
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.NoRecursion);
public CircularRingBuffer(int capacity = 500)
{
if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be positive.");
_capacity = capacity;
_buffer = new T[capacity];
_start = 0;
_count = 0;
}
public int Capacity => _capacity;
public int Count
{
get
{
_lock.EnterReadLock();
try { return _count; }
finally { _lock.ExitReadLock(); }
}
}
/// <summary>
/// Adds an item to the buffer. If capacity is reached, the oldest element is overwritten in O(1).
/// </summary>
public void Add(T item)
{
_lock.EnterWriteLock();
try
{
if (_count < _capacity)
{
int nextIndex = (_start + _count) % _capacity;
_buffer[nextIndex] = item;
_count++;
}
else
{
_buffer[_start] = item;
_start = (_start + 1) % _capacity;
}
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// Updates the last (most recent) item in place.
/// </summary>
public void UpdateLast(T item)
{
_lock.EnterWriteLock();
try
{
if (_count == 0)
{
_buffer[_start] = item;
_count = 1;
}
else
{
int lastIndex = (_start + _count - 1) % _capacity;
_buffer[lastIndex] = item;
}
}
finally
{
_lock.ExitWriteLock();
}
}
/// <summary>
/// Gets the most recent item or default if empty.
/// </summary>
public T? GetLast()
{
_lock.EnterReadLock();
try
{
if (_count == 0) return default;
int lastIndex = (_start + _count - 1) % _capacity;
return _buffer[lastIndex];
}
finally
{
_lock.ExitReadLock();
}
}
/// <summary>
/// Indexer accessing items from oldest (0) to newest (Count - 1).
/// </summary>
public T this[int index]
{
get
{
_lock.EnterReadLock();
try
{
if (index < 0 || index >= _count)
throw new ArgumentOutOfRangeException(nameof(index), "Index out of range.");
int actualIndex = (_start + index) % _capacity;
return _buffer[actualIndex];
}
finally
{
_lock.ExitReadLock();
}
}
}
/// <summary>
/// Returns an ordered immutable array snapshot of all elements.
/// </summary>
public T[] ToArray()
{
_lock.EnterReadLock();
try
{
if (_count == 0) return Array.Empty<T>();
var result = new T[_count];
for (int i = 0; i < _count; i++)
{
int actualIndex = (_start + i) % _capacity;
result[i] = _buffer[actualIndex];
}
return result;
}
finally
{
_lock.ExitReadLock();
}
}
/// <summary>
/// Populates the buffer in bulk with historical data (oldest to newest).
/// </summary>
public void LoadBulk(IEnumerable<T> items)
{
_lock.EnterWriteLock();
try
{
_start = 0;
_count = 0;
foreach (var item in items)
{
if (_count < _capacity)
{
_buffer[_count] = item;
_count++;
}
else
{
_buffer[_start] = item;
_start = (_start + 1) % _capacity;
}
}
}
finally
{
_lock.ExitWriteLock();
}
}
public Enumerator GetEnumerator() => new(this);
IEnumerator<T> IEnumerable<T>.GetEnumerator() => new Enumerator(this);
IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);
public struct Enumerator : IEnumerator<T>
{
private readonly CircularRingBuffer<T> _buffer;
private int _index;
private T? _current;
internal Enumerator(CircularRingBuffer<T> buffer)
{
_buffer = buffer;
_index = 0;
_current = default;
}
public readonly T Current => _current!;
readonly object? IEnumerator.Current => Current;
public bool MoveNext()
{
if (_index < _buffer.Count)
{
_current = _buffer[_index];
_index++;
return true;
}
_current = default;
return false;
}
public void Reset()
{
_index = 0;
_current = default;
}
public readonly void Dispose() { }
}
}