From 4dbba5c2f3d90ad1709d91aee13d32b9cb672835 Mon Sep 17 00:00:00 2001 From: Kyle Ratti Date: Thu, 5 Sep 2024 22:53:48 -0400 Subject: [PATCH] feat: add FirstOrEmptyAsync --- Base.Tests/Structures/MaybeExtensionTests.cs | 89 ++++++++++++++++++++ Base/Structures/MaybeExtensions.cs | 61 +++++++++++++- 2 files changed, 149 insertions(+), 1 deletion(-) diff --git a/Base.Tests/Structures/MaybeExtensionTests.cs b/Base.Tests/Structures/MaybeExtensionTests.cs index 81fc608..da2542b 100644 --- a/Base.Tests/Structures/MaybeExtensionTests.cs +++ b/Base.Tests/Structures/MaybeExtensionTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Threading.Tasks; using FruityFoundation.Base.Structures; using NUnit.Framework; @@ -51,6 +52,86 @@ public class MaybeExtensionTests Assert.That(result.HasValue, Is.False); } + [Test] + public async Task Enumerable_FirstOrEmptyAsync_WithEmptyEnumerable_ReturnsEmptyMaybe() + { + // Arrange + var data = ToAsyncEnumerable(Array.Empty()); + + // Act + var result = await data.FirstOrEmptyAsync(); + + // Assert + Assert.That(result, Is.InstanceOf>()); + Assert.That(result.HasValue, Is.False); + } + + [Test] + public async Task Enumerable_FirstOrEmptyAsync_WithMatchingPredicate_ReturnsMaybeWithValue() + { + // Arrange + var data = ToAsyncEnumerable([1, 2, 3, 4]); + + // Act + var result = await data.FirstOrEmptyAsync(x => x > 1); + + // Assert + Assert.That(result, Is.InstanceOf>()); + Assert.That(result.HasValue, Is.True); + Assert.That(result.Value, Is.EqualTo(2)); + } + + [Test] + public async Task Enumerable_FirstOrEmptyAsync_WithMatchingAsyncPredicate_ReturnsMaybeWithValue() + { + // Arrange + var data = ToAsyncEnumerable([1, 2, 3, 4]); + + // Act + var result = await data.FirstOrEmptyAsync(async x => + { + await Task.Yield(); + return x > 1; + }); + + // Assert + Assert.That(result, Is.InstanceOf>()); + Assert.That(result.HasValue, Is.True); + Assert.That(result.Value, Is.EqualTo(2)); + } + + [Test] + public async Task Enumerable_FirstOrEmptyAsync_WithNonMatchingPredicate_ReturnsEmptyMaybe() + { + // Arrange + var data = ToAsyncEnumerable([1, 2, 3, 4]); + + // Act + var result = await data.FirstOrEmptyAsync(x => x > 100); + + // Assert + Assert.That(result, Is.InstanceOf>()); + Assert.That(result.HasValue, Is.False); + } + + [Test] + public async Task Enumerable_FirstOrEmptyAsync_WithNonMatchingAsyncPredicate_ReturnsEmptyMaybe() + { + // Arrange + var data = ToAsyncEnumerable([1, 2, 3, 4]); + + // Act + var result = await data.FirstOrEmptyAsync(async x => + { + await Task.Yield(); + return x > 100; + }); + + // Assert + Assert.That(result, Is.InstanceOf>()); + Assert.That(result.HasValue, Is.False); + } + [Test] public void MaybeNullableTests() { @@ -125,4 +206,12 @@ public class MaybeExtensionTests Assert.That(result, Is.InstanceOf>()); Assert.That(result.HasValue, Is.False); } + + private static async IAsyncEnumerable ToAsyncEnumerable(IEnumerable enumerable) + { + foreach (var item in enumerable) + yield return item; + + await Task.Yield(); + } } diff --git a/Base/Structures/MaybeExtensions.cs b/Base/Structures/MaybeExtensions.cs index a008744..f5cc08e 100644 --- a/Base/Structures/MaybeExtensions.cs +++ b/Base/Structures/MaybeExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace FruityFoundation.Base.Structures; @@ -22,6 +23,64 @@ public static class MaybeExtensions return Maybe.Empty(); } + public static async ValueTask> FirstOrEmptyAsync(this IAsyncEnumerable source, CancellationToken cancellationToken = default) + { + if (source is IList { Count: > 0 } list) + return Maybe.Create(list[0]); + + await using var e = source + .ConfigureAwait(false) + .WithCancellation(cancellationToken) + .GetAsyncEnumerator(); + + if (await e.MoveNextAsync()) + return e.Current; + + return Maybe.Empty(); + } + + public static async ValueTask> FirstOrEmptyAsync(this IAsyncEnumerable source, Func predicate, CancellationToken cancellationToken = default) + { + if (source is IList { Count: > 0 } list) + return list[0]; + + await using var e = source + .ConfigureAwait(false) + .WithCancellation(cancellationToken) + .GetAsyncEnumerator(); + + while (await e.MoveNextAsync()) + { + var value = e.Current; + + if (predicate(value)) + return value; + } + + return Maybe.Empty(); + } + + public static async ValueTask> FirstOrEmptyAsync(this IAsyncEnumerable source, Func> asyncPredicate, CancellationToken cancellationToken = default) + { + if (source is IList { Count: > 0 } list) + return list[0]; + + await using var e = source + .ConfigureAwait(false) + .WithCancellation(cancellationToken) + .GetAsyncEnumerator(); + + while (await e.MoveNextAsync()) + { + var value = e.Current; + + if (await asyncPredicate(value).ConfigureAwait(false)) + return value; + } + + return Maybe.Empty(); + } + public static Maybe TryGetValue(this IDictionary dict, TKey key) => dict.TryGetValue(key, out var value) ? Maybe.Create(value) : Maybe.Empty();