Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 36 additions & 5 deletions src/Olve.Utilities/Collections/FixedSizeQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,64 @@ public class FixedSizeQueue<T> : IQueue<T>
{
private readonly Queue<T> _queue = new();
private readonly int _maxSize;
private readonly FullQueueBehavior _fullBehavior;

/// <summary>
/// Initializes a new instance of the <see cref="FixedSizeQueue{T}"/> class
/// with the specified maximum size.
/// with the specified maximum size and full-queue behavior.
/// </summary>
/// <param name="maxSize">The maximum number of items the queue can hold. Must be greater than zero.</param>
/// <param name="fullBehavior">The behavior when the queue is full. Defaults to <see cref="FullQueueBehavior.DropOldest"/>.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown when <paramref name="maxSize"/> is less than or equal to zero.
/// </exception>
public FixedSizeQueue(int maxSize)
public FixedSizeQueue(int maxSize, FullQueueBehavior fullBehavior = FullQueueBehavior.DropOldest)
{
if (maxSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(maxSize), "Max size must be greater than 0.");
}

_maxSize = maxSize;
_fullBehavior = fullBehavior;
}

/// <inheritdoc />
public void Enqueue(T item)
public bool Enqueue(T item)
{
if (_queue.Count >= _maxSize)
{
switch (_fullBehavior)
{
case FullQueueBehavior.DropNewest:
return false;
case FullQueueBehavior.Throw:
throw new InvalidOperationException(
$"Queue is full (capacity: {_maxSize}).");
case FullQueueBehavior.DropOldest:
default:
while (_queue.Count >= _maxSize)
{
_queue.Dequeue();
}
break;
}
}

_queue.Enqueue(item);
while (_queue.Count > _maxSize)
return true;
}

/// <inheritdoc />
public bool TryEnqueue(T item)
{
if (_queue.Count >= _maxSize)
{
_queue.Dequeue();
return false;
}

_queue.Enqueue(item);
return true;
}

/// <inheritdoc />
Expand Down
23 changes: 23 additions & 0 deletions src/Olve.Utilities/Collections/FullQueueBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Olve.Utilities.Collections;

/// <summary>
/// Specifies the behavior of a <see cref="FixedSizeQueue{T}"/> when an item
/// is enqueued and the queue is already at maximum capacity.
/// </summary>
public enum FullQueueBehavior
{
/// <summary>
/// Remove the oldest item to make room for the new item. This is the default behavior.
/// </summary>
DropOldest,

/// <summary>
/// Reject the incoming item, leaving the queue unchanged.
/// </summary>
DropNewest,

/// <summary>
/// Throw an <see cref="InvalidOperationException"/>.
/// </summary>
Throw,
}
19 changes: 16 additions & 3 deletions src/Olve.Utilities/Collections/IQueue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,24 @@ public interface IQueue<T> : IEnumerable<T>
{
/// <summary>
/// Adds an item to the queue.
/// If adding the item causes the queue to exceed its maximum size,
/// the oldest item is removed.
/// The behavior when the queue is full depends on the configured <see cref="FullQueueBehavior"/>.
/// </summary>
/// <param name="item">The item to enqueue.</param>
void Enqueue(T item);
/// <returns>
/// <c>true</c> if the item was added to the queue;
/// <c>false</c> if the item was rejected (e.g. when using <see cref="FullQueueBehavior.DropNewest"/>).
/// </returns>
bool Enqueue(T item);

/// <summary>
/// Attempts to add an item to the queue.
/// Returns <c>false</c> if the queue is at capacity, regardless of the configured <see cref="FullQueueBehavior"/>.
/// </summary>
/// <param name="item">The item to enqueue.</param>
/// <returns>
/// <c>true</c> if the item was added; <c>false</c> if the queue is full.
/// </returns>
bool TryEnqueue(T item);

/// <summary>
/// Attempts to remove and return the item at the front of the queue.
Expand Down
34 changes: 25 additions & 9 deletions src/Olve.Utilities/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,17 +131,33 @@ enrollment.TryGet(101, out var mathStudents); // { "alice", "bob" }

### FixedSizeQueue

`FixedSizeQueue<T>` automatically drops the oldest items when the maximum size is exceeded.
`FixedSizeQueue<T>` automatically manages items when the maximum size is exceeded. Configure the behavior with `FullQueueBehavior`: `DropOldest` (default), `DropNewest`, or `Throw`.

```cs
// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L90-L95
// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L90-L111

var queue = new FixedSizeQueue<string>(maxSize: 3);

queue.Enqueue("a");
queue.Enqueue("b");
queue.Enqueue("c");
queue.Enqueue("d"); // "a" is dropped, queue is now { "b", "c", "d" }

queue.TryDequeue(out var first); // "b"

// configure back-pressure behavior
var strict = new FixedSizeQueue<string>(maxSize: 2, FullQueueBehavior.Throw);
strict.Enqueue("x");
strict.Enqueue("y");
// strict.Enqueue("z"); // throws InvalidOperationException

var dropping = new FixedSizeQueue<string>(maxSize: 2, FullQueueBehavior.DropNewest);
dropping.Enqueue("x");
dropping.Enqueue("y");
var accepted = dropping.Enqueue("z"); // false — "z" is rejected

// TryEnqueue fails when full, regardless of policy
var tried = strict.TryEnqueue("z"); // false — queue is at capacity
```

---
Expand All @@ -151,7 +167,7 @@ queue.Enqueue("d"); // "a" is dropped, queue is now { "b", "c", "d" }
`DateTimeFormatter.FormatTimeAgo()` produces human-readable relative time strings like "2 days ago" or "just now".

```cs
// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L104-L107
// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L122-L125

var now = new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero);
var then = new DateTimeOffset(2025, 6, 13, 12, 0, 0, TimeSpan.Zero);
Expand All @@ -166,7 +182,8 @@ var text = DateTimeFormatter.FormatTimeAgo(now, then); // "2 days ago"
`Pagination` computes offsets from page number and size. `PaginatedResult<T>` wraps a page of items with total count and navigation metadata.

```cs
// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L115-L124
// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L132-L141


var items = new[] { "alice", "bob", "charlie" };
var pagination = new Pagination(Page: 0, PageSize: 2);
Expand All @@ -177,7 +194,6 @@ var result = new PaginatedResult<string>(
totalCount: items.Length);

// result.HasNextPage == true
// result.TotalPages == 2
```

---
Expand All @@ -187,7 +203,8 @@ var result = new PaginatedResult<string>(
`DirectedGraph` provides an ID-based directed graph with node and edge management.

```cs
// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L134-L146
// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L151-L163


var graph = new DirectedGraph();

Expand All @@ -201,7 +218,6 @@ graph.CreateEdge(nodeA, nodeB);
graph.CreateEdge(nodeA, nodeC);

// Query outgoing edges
graph.TryGetOutgoingEdges(nodeA, out var edges); // 2 edges
```

---
Expand All @@ -211,7 +227,8 @@ graph.TryGetOutgoingEdges(nodeA, out var edges); // 2 edges
High-performance `GetOrAdd` and `TryUpdate` extensions using `CollectionsMarshal` for zero-overhead dictionary operations.

```cs
// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L155-L165
// ../../tests/Olve.Utilities.Tests/ReadmeDemo.cs#L172-L182


var cache = new Dictionary<string, List<int>>();

Expand All @@ -223,7 +240,6 @@ var same = cache.GetOrAdd("scores", () => []); // same reference as list

// TryUpdate: update only if the key exists
var updated = cache.TryUpdate("scores", old => [..old, 200]); // true
var missed = cache.TryUpdate("missing", _ => []); // false
```

---
Expand Down
146 changes: 145 additions & 1 deletion tests/Olve.Utilities.Tests/Collections/FixedSizeQueueTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ namespace Olve.Utilities.Tests.Collections;

public class FixedSizeQueueTests
{
private static FixedSizeQueue<T> GetNewQueue<T>(int maxSize) => new(maxSize);
private static FixedSizeQueue<T> GetNewQueue<T>(
int maxSize,
FullQueueBehavior fullBehavior = FullQueueBehavior.DropOldest) => new(maxSize, fullBehavior);

[Test]
public async Task Constructor_WithValidMaxSize_InitializesEmptyQueue()
Expand Down Expand Up @@ -168,4 +170,146 @@ public async Task TryDequeue_OnEmptyQueue_ReturnsFalse()
await Assert.That(result).IsFalse();
await Assert.That(dequeuedItem).IsEqualTo(0);
}

[Test]
public async Task Enqueue_DropNewest_WhenFull_RejectsNewItem()
{
// Arrange
var queue = GetNewQueue<int>(2, FullQueueBehavior.DropNewest);
queue.Enqueue(1);
queue.Enqueue(2);

// Act
var result = queue.Enqueue(3);

// Assert
await Assert.That(result).IsFalse();
await Assert.That(queue.Count).IsEqualTo(2);
var items = queue.ToList();
await Assert.That(items[0]).IsEqualTo(1);
await Assert.That(items[1]).IsEqualTo(2);
}

[Test]
public async Task Enqueue_DropNewest_WhenNotFull_AddsItem()
{
// Arrange
var queue = GetNewQueue<int>(3, FullQueueBehavior.DropNewest);

// Act
var result = queue.Enqueue(1);

// Assert
await Assert.That(result).IsTrue();
await Assert.That(queue.Count).IsEqualTo(1);
}

[Test]
public Task Enqueue_Throw_WhenFull_ThrowsInvalidOperationException()
{
// Arrange
var queue = GetNewQueue<int>(2, FullQueueBehavior.Throw);
queue.Enqueue(1);
queue.Enqueue(2);

// Act & Assert
Assert.Throws<InvalidOperationException>(() => queue.Enqueue(3));
return Task.CompletedTask;
}

[Test]
public async Task Enqueue_Throw_WhenNotFull_AddsItem()
{
// Arrange
var queue = GetNewQueue<int>(3, FullQueueBehavior.Throw);

// Act
var result = queue.Enqueue(1);

// Assert
await Assert.That(result).IsTrue();
await Assert.That(queue.Count).IsEqualTo(1);
}

[Test]
public async Task Enqueue_DropOldest_ReturnsTrue()
{
// Arrange
var queue = GetNewQueue<int>(2);
queue.Enqueue(1);
queue.Enqueue(2);

// Act
var result = queue.Enqueue(3);

// Assert
await Assert.That(result).IsTrue();
await Assert.That(queue.Count).IsEqualTo(2);
}

[Test]
public async Task Constructor_DefaultBehavior_IsDropOldest()
{
// Arrange
var queue = new FixedSizeQueue<int>(2);
queue.Enqueue(1);
queue.Enqueue(2);

// Act
queue.Enqueue(3);

// Assert - oldest item (1) should be gone
var items = queue.ToList();
await Assert.That(items[0]).IsEqualTo(2);
await Assert.That(items[1]).IsEqualTo(3);
}

[Test]
public async Task TryEnqueue_WhenNotFull_AddsItem()
{
// Arrange
var queue = GetNewQueue<int>(2);

// Act
var result = queue.TryEnqueue(1);

// Assert
await Assert.That(result).IsTrue();
await Assert.That(queue.Count).IsEqualTo(1);
}

[Test]
public async Task TryEnqueue_WhenFull_ReturnsFalse()
{
// Arrange
var queue = GetNewQueue<int>(2);
queue.Enqueue(1);
queue.Enqueue(2);

// Act
var result = queue.TryEnqueue(3);

// Assert
await Assert.That(result).IsFalse();
await Assert.That(queue.Count).IsEqualTo(2);
}

[Test]
public async Task TryEnqueue_WhenFull_IgnoresFullQueueBehavior()
{
// Arrange — even with DropOldest, TryEnqueue should just return false
var queue = GetNewQueue<int>(2, FullQueueBehavior.DropOldest);
queue.Enqueue(1);
queue.Enqueue(2);

// Act
var result = queue.TryEnqueue(3);

// Assert
await Assert.That(result).IsFalse();
await Assert.That(queue.Count).IsEqualTo(2);
var items = queue.ToList();
await Assert.That(items[0]).IsEqualTo(1);
await Assert.That(items[1]).IsEqualTo(2);
}
}
Loading
Loading