diff --git a/FrameworkFeatureConstants.props b/FrameworkFeatureConstants.props
index 6912a6711..b7e712b17 100644
--- a/FrameworkFeatureConstants.props
+++ b/FrameworkFeatureConstants.props
@@ -21,4 +21,7 @@
$(DefineConstants);REFLECTION_ASSEMBLY_NAME_INFO;REFLECTION_TYPE_NAME;ORDERED_DICTIONARY
+
+ $(DefineConstants);SHUFFLE_EXTENSION;INTEGRATED_ASYNC
+
diff --git a/Funcky.Analyzers/Funcky.Analyzers.Test/Funcky.Analyzers.Test.csproj b/Funcky.Analyzers/Funcky.Analyzers.Test/Funcky.Analyzers.Test.csproj
index 069027b95..c7aa8f6e6 100644
--- a/Funcky.Analyzers/Funcky.Analyzers.Test/Funcky.Analyzers.Test.csproj
+++ b/Funcky.Analyzers/Funcky.Analyzers.Test/Funcky.Analyzers.Test.csproj
@@ -1,7 +1,7 @@
- net9.0
+ net10.0
true
true
diff --git a/Funcky.Async.Test/Funcky.Async.Test.csproj b/Funcky.Async.Test/Funcky.Async.Test.csproj
index d956a47ea..d02c45473 100644
--- a/Funcky.Async.Test/Funcky.Async.Test.csproj
+++ b/Funcky.Async.Test/Funcky.Async.Test.csproj
@@ -1,4 +1,4 @@
-
+
net9.0;net8.0;net7.0
preview
diff --git a/Funcky.Async/Extensions/AsyncEnumerableExtensions/Scan.cs b/Funcky.Async/Extensions/AsyncEnumerableExtensions/Scan.cs
index 792d0b69c..ec08a2987 100644
--- a/Funcky.Async/Extensions/AsyncEnumerableExtensions/Scan.cs
+++ b/Funcky.Async/Extensions/AsyncEnumerableExtensions/Scan.cs
@@ -5,7 +5,7 @@ namespace Funcky.Extensions;
public static partial class AsyncEnumerableExtensions
{
///
- /// Scan generates a sequence known as the the inclusive prefix sum.
+ /// Scan generates a sequence known as the inclusive prefix sum.
///
/// The type of the source elements.
/// The seed and target type.
@@ -17,7 +17,7 @@ public static IAsyncEnumerable InclusiveScan(
=> InclusiveScanEnumerable(source, seed, accumulator);
///
- /// Scan generates a sequence known as the the inclusive prefix sum.
+ /// Scan generates a sequence known as the inclusive prefix sum.
///
/// The type of the source elements.
/// The seed and target type.
@@ -29,7 +29,7 @@ public static IAsyncEnumerable InclusiveScanAwait InclusiveScanAwaitEnumerable(source, seed, accumulator);
///
- /// Scan generates a sequence known as the the inclusive prefix sum.
+ /// Scan generates a sequence known as the inclusive prefix sum.
///
/// The type of the source elements.
/// The seed and target type.
@@ -41,7 +41,7 @@ public static IAsyncEnumerable InclusiveScanAwaitWithCancellation InclusiveScanAwaitWithCancellationEnumerable(source, seed, accumulator);
///
- /// Scan generates a sequence known as the the exclusive prefix sum.
+ /// Scan generates a sequence known as the exclusive prefix sum.
///
/// The type of the source elements.
/// The seed and target type.
@@ -53,7 +53,7 @@ public static IAsyncEnumerable ExclusiveScan(
=> ExclusiveScanEnumerable(source, seed, accumulator);
///
- /// Scan generates a sequence known as the the exclusive prefix sum.
+ /// Scan generates a sequence known as the exclusive prefix sum.
///
/// The type of the source elements.
/// The seed and target type.
@@ -65,7 +65,7 @@ public static IAsyncEnumerable ExclusiveScanAwait ExclusiveScanAwaitEnumerable(source, seed, accumulator);
///
- /// Scan generates a sequence known as the the exclusive prefix sum.
+ /// Scan generates a sequence known as the exclusive prefix sum.
///
/// The type of the source elements.
/// The seed and target type.
diff --git a/Funcky.SourceGenerator.Test/Funcky.SourceGenerator.Test.csproj b/Funcky.SourceGenerator.Test/Funcky.SourceGenerator.Test.csproj
index b50fdf5a8..6eec76836 100644
--- a/Funcky.SourceGenerator.Test/Funcky.SourceGenerator.Test.csproj
+++ b/Funcky.SourceGenerator.Test/Funcky.SourceGenerator.Test.csproj
@@ -3,7 +3,7 @@
Funcky.SourceGenerator.Test
Funcky.SourceGenerator.Test
- net9.0
+ net10.0
enable
enable
preview
diff --git a/Funcky.Test/AsyncGenerator.cs b/Funcky.Test/AsyncGenerator.cs
new file mode 100644
index 000000000..5d5979388
--- /dev/null
+++ b/Funcky.Test/AsyncGenerator.cs
@@ -0,0 +1,28 @@
+#if INTEGRATED_ASYNC
+using FsCheck;
+using FsCheck.Fluent;
+
+namespace Funcky.Async.Test;
+
+internal static class AsyncGenerator
+{
+ public static Arbitrary> GenerateAsyncEnumerable(IArbMap map)
+ => map.GeneratorFor>().Select(list => list.ToAsyncEnumerable()).ToArbitrary();
+
+ public static Arbitrary> GenerateAwaitSelector(IArbMap map)
+ => map.GeneratorFor>().Select(ResultToValueTask).ToArbitrary();
+
+ public static Arbitrary> GenerateAwaitWithCancellationSelector(IArbMap map)
+ => map.GeneratorFor>().Select(ResultToValueTaskX).ToArbitrary();
+
+ private static AwaitSelector ResultToValueTask(Func f)
+ => new(value => ValueTask.FromResult(f(value)));
+
+ private static AwaitSelectorWithCancellation ResultToValueTaskX(Func f)
+ => new((value, _) => ValueTask.FromResult(f(value)));
+}
+
+public sealed record AwaitSelector(Func> Get);
+
+public sealed record AwaitSelectorWithCancellation(Func> Get);
+#endif
diff --git a/Funcky.Test/AsyncSequence/ConcatTest.cs b/Funcky.Test/AsyncSequence/ConcatTest.cs
new file mode 100644
index 000000000..f46884133
--- /dev/null
+++ b/Funcky.Test/AsyncSequence/ConcatTest.cs
@@ -0,0 +1,40 @@
+#if INTEGRATED_ASYNC
+using System.Collections.Immutable;
+using FsCheck;
+using FsCheck.Fluent;
+using FsCheck.Xunit;
+using Funcky.Async.Test.TestUtilities;
+
+namespace Funcky.Async.Test;
+
+public sealed class ConcatTest
+{
+ [Fact]
+ public async Task ConcatenatedSequenceIsEmptyWhenNoSourcesAreProvidedAsync()
+ {
+ await AsyncAssert.Empty(AsyncSequence.Concat());
+ }
+
+ [Fact]
+ public async Task ConcatenatedSequenceIsEmptyWhenAllSourcesAreEmptyAsync()
+ {
+ await AsyncAssert.Empty(AsyncSequence.Concat(Enumerable.Empty().ToAsyncEnumerable(), Enumerable.Empty().ToAsyncEnumerable(), Enumerable.Empty().ToAsyncEnumerable()));
+ }
+
+ [Property]
+ public Property ConcatenatedSequenceContainsElementsFromAllSourcesInOrder(int[][] sources)
+ {
+ var expected = sources.Aggregate(ImmutableArray.Empty, (l, s) => l.AddRange(s)).ToAsyncEnumerable();
+
+ var innerOuterAsync = sources.Select(source => source.ToAsyncEnumerable()).ToAsyncEnumerable();
+ var innerAsync = sources.Select(source => source.ToAsyncEnumerable());
+ IAsyncEnumerable> outerAsync = sources.ToAsyncEnumerable();
+
+ var result = expected.SequenceEqualAsync(AsyncSequence.Concat(innerOuterAsync)).Result
+ && expected.SequenceEqualAsync(AsyncSequence.Concat(innerAsync)).Result
+ && expected.SequenceEqualAsync(AsyncSequence.Concat(outerAsync)).Result;
+
+ return result.ToProperty();
+ }
+}
+#endif
diff --git a/Funcky.Test/AsyncSequence/CycleRangeTest.cs b/Funcky.Test/AsyncSequence/CycleRangeTest.cs
new file mode 100644
index 000000000..407c3e7b1
--- /dev/null
+++ b/Funcky.Test/AsyncSequence/CycleRangeTest.cs
@@ -0,0 +1,71 @@
+#if INTEGRATED_ASYNC
+using FsCheck;
+using FsCheck.Fluent;
+using FsCheck.Xunit;
+using Funcky.Async.Test.TestUtilities;
+using Funcky.Test.TestUtilities;
+
+namespace Funcky.Async.Test;
+
+public sealed class CycleRangeTest
+{
+ [Fact]
+ public async Task CycleRangeIsEnumeratedLazilyAsync()
+ {
+ var doNotEnumerate = new FailOnEnumerateAsyncSequence();
+
+ await using var cycleRange = AsyncSequence.CycleRange(doNotEnumerate);
+ }
+
+ [Fact]
+ public async Task CyclingAnEmptySetThrowsAnArgumentException()
+ => await Assert.ThrowsAsync(CycleEmptySequenceAsync);
+
+ [Property]
+ public Property CycleRangeCanProduceArbitraryManyItemsAsync(NonEmptySet sequence, PositiveInt arbitraryElements)
+ => (GetArbitraryManyItemsAsync(sequence.Get, arbitraryElements.Get).Result == arbitraryElements.Get)
+ .ToProperty();
+
+ [Property(Skip = "Tofix")]
+ public Property CycleRangeRepeatsTheElementsArbitraryManyTimes(NonEmptySet sequence, PositiveInt arbitraryElements)
+ => CycleRangeRepeatsTheElementsArbitraryManyTimesAsync(sequence.Get.ToAsyncEnumerable(), arbitraryElements.Get)
+ .Result.ToProperty();
+
+ [Fact]
+ public async Task CycleRangeEnumeratesUnderlyingEnumerableOnlyOnceAsync()
+ {
+ var sequence = Sequence.Return("Test", "Hello", "Do", "Wait");
+ var enumerateOnce = AsyncEnumerateOnce.Create(sequence);
+
+ await using var cycleRange = AsyncSequence.CycleRange(enumerateOnce);
+
+ _ = await cycleRange
+ .Take(sequence.Count * 3)
+ .ToListAsync();
+ }
+
+ private static async Task GetArbitraryManyItemsAsync(IEnumerable sequence, int arbitraryElements)
+ {
+ await using var cycleRange = AsyncSequence.CycleRange(sequence.ToAsyncEnumerable());
+
+ return await cycleRange.Take(arbitraryElements).CountAsync();
+ }
+
+ private static async Task CycleEmptySequenceAsync()
+ {
+ await using var cycledRange = AsyncSequence.CycleRange(AsyncSequence.Return());
+ await using var enumerator = cycledRange.GetAsyncEnumerator();
+
+ await enumerator.MoveNextAsync();
+ }
+
+ private static async Task CycleRangeRepeatsTheElementsArbitraryManyTimesAsync(IAsyncEnumerable asyncEnumerable, int arbitraryElements)
+ {
+ await using var cycleRange = AsyncSequence.CycleRange(asyncEnumerable);
+
+ return await cycleRange
+ .IsSequenceRepeating(asyncEnumerable)
+ .NTimes(arbitraryElements);
+ }
+}
+#endif
diff --git a/Funcky.Test/AsyncSequence/CycleTest.cs b/Funcky.Test/AsyncSequence/CycleTest.cs
new file mode 100644
index 000000000..d2f80ddb1
--- /dev/null
+++ b/Funcky.Test/AsyncSequence/CycleTest.cs
@@ -0,0 +1,25 @@
+#if INTEGRATED_ASYNC
+using FsCheck;
+using FsCheck.Fluent;
+using FsCheck.Xunit;
+using Funcky.Test.TestUtilities;
+
+namespace Funcky.Async.Test;
+
+public sealed class CycleTest
+{
+ [Property]
+ public Property CycleCanProduceArbitraryManyItems(int value, PositiveInt arbitraryElements)
+ => (AsyncSequence.Cycle(value).Take(arbitraryElements.Get).CountAsync().Result == arbitraryElements.Get)
+ .ToProperty();
+
+ [Property]
+ public Property CycleRepeatsTheElementArbitraryManyTimes(int value, PositiveInt arbitraryElements)
+ => AsyncSequence
+ .Cycle(value)
+ .IsSequenceRepeating(AsyncSequence.Return(value))
+ .NTimes(arbitraryElements.Get)
+ .Result
+ .ToProperty();
+}
+#endif
diff --git a/Funcky.Test/AsyncSequence/RepeatRangeTest.cs b/Funcky.Test/AsyncSequence/RepeatRangeTest.cs
new file mode 100644
index 000000000..88a01e533
--- /dev/null
+++ b/Funcky.Test/AsyncSequence/RepeatRangeTest.cs
@@ -0,0 +1,101 @@
+#if INTEGRATED_ASYNC
+using FsCheck;
+using FsCheck.Fluent;
+using FsCheck.Xunit;
+using Funcky.Async.Test.TestUtilities;
+using Funcky.Test.TestUtilities;
+
+namespace Funcky.Async.Test;
+
+public sealed class RepeatRangeTest
+{
+ [Fact]
+ public async Task RepeatRangeIsEnumeratedLazily()
+ {
+ var doNotEnumerate = new FailOnEnumerateAsyncSequence();
+
+ await using var repeatRange = AsyncSequence.RepeatRange(doNotEnumerate, 2);
+ }
+
+ [Fact]
+ public async Task RepeatRangeThrowsWhenAlreadyDisposedAsync()
+ {
+ var repeatRange = AsyncSequence.RepeatRange(AsyncSequence.Return(1337), 5);
+
+#pragma warning disable IDISP016 // we test behaviour after Dispose
+#pragma warning disable IDISP017 // we test behaviour after Dispose
+ await repeatRange.DisposeAsync();
+#pragma warning restore IDISP016
+#pragma warning restore IDISP017
+
+ await Assert.ThrowsAsync(async () => await repeatRange.ToListAsync());
+ }
+
+ [Fact]
+ public async Task RepeatRangeThrowsWhenAlreadyDisposedEvenIfYouDisposeBetweenMoveNextAsync()
+ {
+ var list = AsyncSequence.Return(1337, 2, 5);
+
+ const int repeats = 5;
+
+ foreach (var i in Enumerable.Range(0, await list.CountAsync() * repeats))
+ {
+ var repeatRange = AsyncSequence.RepeatRange(list, repeats);
+ await using var enumerator = repeatRange.GetAsyncEnumerator();
+
+ Assert.True(await AsyncEnumerable.Range(0, i).AllAsync(async (_, _) => await enumerator.MoveNextAsync()));
+
+#pragma warning disable IDISP016 // we test behaviour after Dispose
+#pragma warning disable IDISP017 // we test behaviour after Dispose
+ await repeatRange.DisposeAsync();
+#pragma warning restore IDISP016
+#pragma warning restore IDISP017
+
+ await Assert.ThrowsAnyAsync(async () => await enumerator.MoveNextAsync());
+ }
+ }
+
+ [Property]
+ public Property TheLengthOfTheGeneratedRepeatRangeIsCorrect(List list, NonNegativeInt count)
+ => TheLengthOfTheGeneratedRepeatRangeIsCorrectAsync(list, count.Get)
+ .Result
+ .ToProperty();
+
+ [Property(Skip = "Tofix")]
+ public Property TheSequenceRepeatsTheGivenNumberOfTimes(List list, NonNegativeInt count)
+ => TheSequenceRepeatsTheGivenNumberOfTimesAsync(list.ToAsyncEnumerable(), count.Get)
+ .Result
+ .ToProperty();
+
+ [Fact]
+ public async Task RepeatRangeEnumeratesUnderlyingEnumerableOnlyOnceAsync()
+ {
+ var sequence = Sequence.Return("Test", "Hello", "Do", "Wait");
+ var enumerateOnce = AsyncEnumerateOnce.Create(sequence);
+
+ await using var repeatRange = AsyncSequence.RepeatRange(enumerateOnce, 3);
+
+ await foreach (var dummy in repeatRange)
+ {
+ }
+ }
+
+ private static async Task TheLengthOfTheGeneratedRepeatRangeIsCorrectAsync(List list, int count)
+ {
+ await using var repeatRange = AsyncSequence.RepeatRange(list.ToAsyncEnumerable(), count);
+
+ var materialized = await repeatRange.ToListAsync();
+
+ return materialized.Count == list.Count * count;
+ }
+
+ private static async Task TheSequenceRepeatsTheGivenNumberOfTimesAsync(IAsyncEnumerable asyncEnumerable, int count)
+ {
+ await using var repeatRange = AsyncSequence.RepeatRange(asyncEnumerable, count);
+
+ return await repeatRange
+ .IsSequenceRepeating(asyncEnumerable)
+ .NTimes(count);
+ }
+}
+#endif
diff --git a/Funcky.Test/AsyncSequence/ReturnTest.cs b/Funcky.Test/AsyncSequence/ReturnTest.cs
new file mode 100644
index 000000000..f57bf71c0
--- /dev/null
+++ b/Funcky.Test/AsyncSequence/ReturnTest.cs
@@ -0,0 +1,35 @@
+#if INTEGRATED_ASYNC
+using FsCheck;
+using FsCheck.Fluent;
+using FsCheck.Xunit;
+using Funcky.Async.Test.TestUtilities;
+
+namespace Funcky.Async.Test;
+
+public sealed class ReturnTest
+{
+ [Property]
+ public Property ReturnOfASingleItemElevatesThatItemIntoASingleItemedEnumerable(int item)
+ {
+ var sequence = AsyncSequence.Return(item);
+
+ return (sequence.SingleOrNoneAsync().Result == item).ToProperty();
+ }
+
+ [Fact]
+ public async Task SequenceReturnCreatesAnEnumerableFromAnArbitraryNumberOfParameters()
+ {
+ const string one = "Alpha";
+ const string two = "Beta";
+ const string three = "Gamma";
+
+ var sequence = AsyncSequence.Return(one, two, three);
+
+ await AsyncAssert.Collection(
+ sequence,
+ element1 => Assert.Equal(one, element1),
+ element2 => Assert.Equal(two, element2),
+ element3 => Assert.Equal(three, element3));
+ }
+}
+#endif
diff --git a/Funcky.Test/AsyncSequence/SuccessorsTest.cs b/Funcky.Test/AsyncSequence/SuccessorsTest.cs
new file mode 100644
index 000000000..7b3645b63
--- /dev/null
+++ b/Funcky.Test/AsyncSequence/SuccessorsTest.cs
@@ -0,0 +1,44 @@
+#if INTEGRATED_ASYNC
+using Funcky.Async.Test.TestUtilities;
+
+namespace Funcky.Async.Test;
+
+public sealed class SuccessorsTest
+{
+ [Fact]
+ public async Task ReturnsEmptySequenceWhenFirstItemIsNoneAsync()
+ {
+ await AsyncAssert.Empty(AsyncSequence.Successors(Option.None, ValueTask.FromResult));
+ }
+
+ [Fact]
+ public async Task ReturnsOnlyTheFirstItemWhenSuccessorFunctionImmediatelyReturnsNoneAsync()
+ {
+ var first = await AsyncAssert.Single(AsyncSequence.Successors(10, _ => ValueTask.FromResult(Option.None)));
+ Assert.Equal(10, first);
+ }
+
+ [Fact]
+ public async Task SuccessorsWithNonOptionFunctionReturnsEndlessEnumerableAsync()
+ {
+ const int count = 40;
+ Assert.Equal(count, await AsyncSequence.Successors(0, ValueTask.FromResult).Take(count).CountAsync());
+ }
+
+ [Fact]
+ public async Task SuccessorsReturnsEnumerableThatReturnsValuesBasedOnSeedAsync()
+ {
+ await AsyncAssert.Equal(
+ AsyncEnumerable.Range(0, 10),
+ AsyncSequence.Successors(0, i => ValueTask.FromResult(i + 1)).Take(10));
+ }
+
+ [Fact]
+ public async Task SuccessorsReturnsEnumerableThatReturnsItemUntilNoneIsReturnedFromFuncAsync()
+ {
+ await AsyncAssert.Equal(
+ AsyncEnumerable.Range(0, 11),
+ AsyncSequence.Successors(0, i => ValueTask.FromResult(Option.FromBoolean(i < 10, i + 1))));
+ }
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/AdjacentGroupByTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/AdjacentGroupByTest.cs
new file mode 100644
index 000000000..b5ce892b3
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/AdjacentGroupByTest.cs
@@ -0,0 +1,124 @@
+#if INTEGRATED_ASYNC
+using Funcky.Async.Test.TestUtilities;
+using Funcky.Test.TestUtilities;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class AdjacentGroupByTest
+{
+ private const int DaysInAYear = 365;
+ private const int DaysInALeapYear = 366;
+ private const int MonthsInAYear = 12;
+ private const int February = 1;
+ private const int DaysInFebruaryInLeapYears = 29;
+
+ [Fact]
+ public void AdjacentGroupByIsEnumeratedLazily()
+ {
+ var doNotEnumerate = new FailOnEnumerateAsyncSequence();
+
+ _ = doNotEnumerate.AdjacentGroupBy(FailOnCall.Function);
+ _ = doNotEnumerate.AdjacentGroupBy(FailOnCall.Function, EqualityComparer.Default);
+ _ = doNotEnumerate.AdjacentGroupBy(FailOnCall.Function, FailOnCall.Function);
+ _ = doNotEnumerate.AdjacentGroupBy(FailOnCall.Function, FailOnCall.Function, IEnumerable>);
+ _ = doNotEnumerate.AdjacentGroupBy(FailOnCall.Function, FailOnCall.Function, EqualityComparer.Default);
+ _ = doNotEnumerate.AdjacentGroupBy(FailOnCall.Function, FailOnCall.Function, IEnumerable>, EqualityComparer.Default);
+ _ = doNotEnumerate.AdjacentGroupBy(FailOnCall.Function, FailOnCall.Function, FailOnCall.Function, IEnumerable>);
+ _ = doNotEnumerate.AdjacentGroupBy(FailOnCall.Function, FailOnCall.Function, FailOnCall.Function, IEnumerable>, EqualityComparer.Default);
+ }
+
+ [Fact]
+ public async Task GivenAnEmptySequenceAnyKeySelectorReturnsAnEmptySequence()
+ {
+ var empty = AsyncEnumerable.Empty();
+
+ await AsyncAssert.Empty(empty.AdjacentGroupBy(date => date.Month));
+ await AsyncAssert.Empty(empty.AdjacentGroupBy(date => date.DayOfYear / 7));
+ await AsyncAssert.Empty(empty.AdjacentGroupBy(date => date.Year));
+ }
+
+ [Fact]
+ public async Task GivenConstantKeySelectorOneGroupWithTheConstantsKeyWillBeSelected()
+ {
+ const int groupKey = 42;
+ const int elementCount = 20;
+ var range = AsyncEnumerable.Range(0, elementCount);
+ var group = range.AdjacentGroupBy(_ => groupKey);
+
+ var grouping = await AsyncAssert.Single(group);
+ Assert.Equal(groupKey, grouping.Key);
+ Assert.Equal(elementCount, grouping.Count());
+ }
+
+ [Fact]
+ public async Task GivenASelectorSwitchingBetween0And1WeGetANewSequenceOnEachSwitch()
+ {
+ var range = AsyncEnumerable.Range(0, 100);
+
+ Assert.Equal(25, await range.AdjacentGroupBy(n => (n / 4) % 2).CountAsync());
+ }
+
+ [Fact]
+ public async Task GivenAYearGroupByCreatesMonthsCorrectly()
+ {
+ var dates = DateGenerator(2020);
+
+ var months = dates.AdjacentGroupBy(date => date.Month);
+
+ Assert.Equal(DaysInALeapYear, await dates.CountAsync());
+ Assert.Equal(MonthsInAYear, await months.CountAsync());
+ await AsyncAssert.Equal(DaysInMonthsOfALeapYear(), months.Select((month, _) => month.Count()));
+ }
+
+ [Fact]
+ public async Task GivenTwoYearsGroupByAdjacentGroupsJanuaryOfTwoDifferentYearsInTwoDifferentGroups()
+ {
+ var dates = DateGenerator(2019, 2020);
+
+ var months = dates.AdjacentGroupBy(date => date.Month);
+
+ Assert.Equal(DaysInAYear + DaysInALeapYear, await dates.CountAsync());
+ Assert.Equal(2 * MonthsInAYear, await months.CountAsync());
+ await AsyncAssert.Equal(DaysInMonthsOfAYear().Concat(DaysInMonthsOfALeapYear()), months.Select((month, _) => month.Count()));
+ }
+
+ [Fact]
+ public async Task GivenAdjacentGroupByWithResultSelectorProjectsTheResultCorrectly()
+ {
+ var dates = DateGenerator(2020);
+
+ var months = dates.AdjacentGroupBy(date => date.Month, (_, list) => list.Count());
+
+ await AsyncAssert.Equal(DaysInMonthsOfALeapYear(), months);
+ }
+
+ [Fact]
+ public async Task GivenAdjacentGroupByWithElementSelectorProjectsTheResultCorrectly()
+ {
+ var numbers = AsyncEnumerable.Range(1, 5);
+
+ var grouped = numbers.AdjacentGroupBy(number => number / 3, number => number * -1);
+ Assert.Equal("-3,-4,-5", string.Join(",", await grouped.LastAsync()));
+ }
+
+#pragma warning disable CS1998
+ private static async IAsyncEnumerable DateGenerator(int startYear, Option endYear = default)
+#pragma warning restore CS1998
+ {
+ var current = new DateTime(startYear, 1, 1);
+
+ while (current.Year <= endYear.GetOrElse(startYear))
+ {
+ yield return current;
+ current = current.AddDays(1);
+ }
+ }
+
+ private static IAsyncEnumerable DaysInMonthsOfAYear()
+ => AsyncSequence.Return(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
+
+ private static IAsyncEnumerable DaysInMonthsOfALeapYear()
+ => DaysInMonthsOfAYear()
+ .Select((value, index) => index == February ? DaysInFebruaryInLeapYears : value);
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/AnyOrElseTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/AnyOrElseTest.cs
new file mode 100644
index 000000000..7e737ce8b
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/AnyOrElseTest.cs
@@ -0,0 +1,44 @@
+#if INTEGRATED_ASYNC
+using Funcky.Async.Test.TestUtilities;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class AnyOrElseTest
+{
+ [Fact]
+ public void SourceIsEnumeratedLazily()
+ {
+ var source = new FailOnEnumerateAsyncSequence();
+ _ = source.AnyOrElse(AsyncEnumerable.Empty());
+ }
+
+ [Fact]
+ public void FallbackIsEnumeratedLazily()
+ {
+ var source = AsyncEnumerable.Empty().Select(Identity);
+ _ = source.AnyOrElse(new FailOnEnumerateAsyncSequence());
+ }
+
+ [Fact]
+ public async Task IsEmptyWhenBothEnumerablesAreEmpty()
+ {
+ await AsyncAssert.Empty(AsyncEnumerable.Empty().AnyOrElse(AsyncEnumerable.Empty()));
+ }
+
+ [Fact]
+ public async Task IsSourceEnumerableWhenNonEmpty()
+ {
+ var source = AsyncSequence.Return(1, 2, 3);
+ var fallback = AsyncSequence.Return(4, 5, 6);
+ await AsyncAssert.Equal(source, source.AnyOrElse(fallback));
+ }
+
+ [Fact]
+ public async Task IsFallbackEnumerableWhenSourceIsEmpty()
+ {
+ var source = AsyncEnumerable.Empty();
+ var fallback = AsyncSequence.Return(1, 2, 3);
+ await AsyncAssert.Equal(fallback, source.AnyOrElse(fallback));
+ }
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/AverageOrNoneTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/AverageOrNoneTest.cs
new file mode 100644
index 000000000..9815c0986
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/AverageOrNoneTest.cs
@@ -0,0 +1,161 @@
+#if INTEGRATED_ASYNC
+// ReSharper disable PossibleMultipleEnumeration
+using FsCheck;
+using FsCheck.Fluent;
+using Funcky.Test.Internal;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class AverageOrNoneTest
+{
+ // Int32/int Tests
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncGivesTheSameResultAsAverageAsyncForInt32(IAsyncEnumerable sequence)
+ => CompareAverageAndHandleEmptyInt32SequenceAsync(sequence).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncGivesTheSameResultAsAverageForNullableAsyncForInt32(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForInt32(IAsyncEnumerable sequence, Func selector)
+ => (Option.FromNullable(sequence.Select(selector).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAsync(SelectorTransformation.TransformNullableSelector(selector)).Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAwaitAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForInt32(IAsyncEnumerable sequence, AwaitSelector selector)
+ => (Option.FromNullable(sequence.Select((int? item, CancellationToken _) => selector.Get(item)).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAwaitAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageAwaitWithCancellationAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForInt32(IAsyncEnumerable sequence, AwaitSelectorWithCancellation selector)
+ => (Option.FromNullable(sequence.Select(selector.Get).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAwaitWithCancellationAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ // Int64/long Tests
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncGivesTheSameResultAsAverageAsyncForInt64(IAsyncEnumerable sequence)
+ => CompareAverageAndHandleEmptyInt64SequenceAsync(sequence).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncGivesTheSameResultAsAverageForNullableAsyncForInt64(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForInt64(IAsyncEnumerable sequence, Func selector)
+ => (Option.FromNullable(sequence.Select(selector).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAsync(SelectorTransformation.TransformNullableSelector(selector)).Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAwaitAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForInt64(IAsyncEnumerable sequence, AwaitSelector selector)
+ => (Option.FromNullable(sequence.Select((long? item, CancellationToken _) => selector.Get(item)).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAwaitAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageAwaitWithCancellationAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForInt64(IAsyncEnumerable sequence, AwaitSelectorWithCancellation selector)
+ => (Option.FromNullable(sequence.Select(selector.Get).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAwaitWithCancellationAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ // Single/float Tests
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncGivesTheSameResultAsAverageAsyncForSingle(IAsyncEnumerable sequence)
+ => CompareAverageAndHandleEmptySingleSequenceAsync(sequence).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncGivesTheSameResultAsAverageForNullableAsyncForSingle(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForSingle(IAsyncEnumerable sequence, Func selector)
+ => (Option.FromNullable(sequence.Select(selector).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAsync(SelectorTransformation.TransformNullableSelector(selector)).Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAwaitAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForSingle(IAsyncEnumerable sequence, AwaitSelector selector)
+ => (Option.FromNullable(sequence.Select((float? item, CancellationToken _) => selector.Get(item)).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAwaitAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageAwaitWithCancellationAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForSingle(IAsyncEnumerable sequence, AwaitSelectorWithCancellation selector)
+ => (Option.FromNullable(sequence.Select(selector.Get).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAwaitWithCancellationAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ // Double/double Tests
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncGivesTheSameResultAsAverageAsyncForDouble(IAsyncEnumerable sequence)
+ => CompareAverageAndHandleEmptyDoubleSequenceAsync(sequence).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncGivesTheSameResultAsAverageForNullableAsyncForDouble(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForDouble(IAsyncEnumerable sequence, Func selector)
+ => (Option.FromNullable(sequence.Select(selector).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAsync(SelectorTransformation.TransformNullableSelector(selector)).Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAwaitAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForDouble(IAsyncEnumerable sequence, AwaitSelector selector)
+ => (Option.FromNullable(sequence.Select((double? item, CancellationToken _) => selector.Get(item)).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAwaitAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageAwaitWithCancellationAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForDouble(IAsyncEnumerable sequence, AwaitSelectorWithCancellation selector)
+ => (Option.FromNullable(sequence.Select(selector.Get).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAwaitWithCancellationAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ // Decimal/decimal Tests
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncGivesTheSameResultAsAverageAsyncForDecimal(IAsyncEnumerable sequence)
+ => CompareAverageAndHandleEmptyDecimalSequenceAsync(sequence).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncGivesTheSameResultAsAverageForNullableAsyncForDecimal(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForDecimal(IAsyncEnumerable sequence, Func selector)
+ => (Option.FromNullable(sequence.Select(selector).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAsync(SelectorTransformation.TransformNullableSelector(selector)).Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageOrNoneAwaitAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForDecimal(IAsyncEnumerable sequence, AwaitSelector selector)
+ => (Option.FromNullable(sequence.Select((decimal? item, CancellationToken _) => selector.Get(item)).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAwaitAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property AverageAwaitWithCancellationAsyncWithSelectorGivesTheSameResultAsAverageForNullableAsyncForDecimal(IAsyncEnumerable sequence, AwaitSelectorWithCancellation selector)
+ => (Option.FromNullable(sequence.Select(selector.Get).AverageAsync().Result)
+ == sequence.Select(Option.FromNullable).AverageOrNoneAwaitWithCancellationAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ private static async Task CompareAverageAndHandleEmptyInt32SequenceAsync(IAsyncEnumerable sequence)
+ => await sequence.AnyAsync()
+ ? await sequence.AverageAsync() == await sequence.AverageOrNoneAsync()
+ : (await sequence.AverageOrNoneAsync()).Match(none: true, some: _ => false);
+
+ private static async Task CompareAverageAndHandleEmptyInt64SequenceAsync(IAsyncEnumerable sequence)
+ => await sequence.AnyAsync()
+ ? await sequence.AverageAsync() == await sequence.AverageOrNoneAsync()
+ : (await sequence.AverageOrNoneAsync()).Match(none: true, some: _ => false);
+
+ private static async Task CompareAverageAndHandleEmptySingleSequenceAsync(IAsyncEnumerable sequence)
+ => await sequence.AnyAsync()
+ ? await sequence.AverageAsync() == await sequence.AverageOrNoneAsync()
+ : (await sequence.AverageOrNoneAsync()).Match(none: true, some: _ => false);
+
+ private static async Task CompareAverageAndHandleEmptyDoubleSequenceAsync(IAsyncEnumerable sequence)
+ => await sequence.AnyAsync()
+ ? await sequence.AverageAsync() == await sequence.AverageOrNoneAsync()
+ : (await sequence.AverageOrNoneAsync()).Match(none: true, some: _ => false);
+
+ private static async Task CompareAverageAndHandleEmptyDecimalSequenceAsync(IAsyncEnumerable sequence)
+ => await sequence.AnyAsync()
+ ? await sequence.AverageAsync() == await sequence.AverageOrNoneAsync()
+ : (await sequence.AverageOrNoneAsync()).Match(none: true, some: _ => false);
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/ConcatToStringTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/ConcatToStringTest.cs
new file mode 100644
index 000000000..2f888768a
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/ConcatToStringTest.cs
@@ -0,0 +1,46 @@
+#if INTEGRATED_ASYNC
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public class ConcatToStringTest
+{
+ [Fact]
+ public async Task ConcatenatingAnEmptySetOfStringsReturnsAnEmptyString()
+ {
+ var empty = AsyncEnumerable.Empty();
+
+ Assert.Equal(string.Empty, await empty.ConcatToStringAsync());
+ }
+
+ [Fact]
+ public async Task ConcatenatingASetWithExactlyOneElementReturnsTheElement()
+ {
+ var singleElement = AsyncSequence.Return("Alpha");
+
+ Assert.Equal("Alpha", await singleElement.ConcatToStringAsync());
+ }
+
+ [Fact]
+ public async Task ConcatenatingAListOfStringsReturnsAllElementsWithoutASeparator()
+ {
+ var strings = AsyncSequence.Return("Alpha", "Beta", "Gamma");
+
+ Assert.Equal("AlphaBetaGamma", await strings.ConcatToStringAsync());
+ }
+
+ [Fact]
+ public async Task ConcatenatingNonStringsWorksToo()
+ {
+ var numbers = AsyncSequence.Return(1, 2, 3);
+
+ Assert.Equal("123", await numbers.ConcatToStringAsync());
+ }
+
+ [Fact]
+ public async Task NullsAreHandledAsEmptyStringsWhileConcatenating()
+ {
+ var strings = AsyncSequence.Return("Alpha", null, "Gamma");
+
+ Assert.Equal("AlphaGamma", await strings.ConcatToStringAsync());
+ }
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/ElementAtOrNoneTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/ElementAtOrNoneTest.cs
new file mode 100644
index 000000000..f2ddcbd48
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/ElementAtOrNoneTest.cs
@@ -0,0 +1,43 @@
+#if INTEGRATED_ASYNC
+using FsCheck;
+using FsCheck.Fluent;
+using Funcky.FsCheck;
+using static Funcky.Async.Test.Extensions.AsyncEnumerableExtensions.TestData;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class ElementAtOrNoneTest
+{
+ [Theory]
+ [InlineData(-42)]
+ [InlineData(-1)]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(42)]
+ public async Task ElementAtOrNoneReturnsAlwaysANoneOnAnEmptyEnumerable(int index)
+ {
+ FunctionalAssert.None(await EmptyEnumerable.ElementAtOrNoneAsync(index));
+ }
+
+ [Fact]
+ public async Task ElementAtOrNoneReturnsSomeWithinTheRangeAndNoneOutside()
+ {
+ FunctionalAssert.None(await EnumerableWithMoreThanOneItem.ElementAtOrNoneAsync(-10));
+ FunctionalAssert.None(await EnumerableWithMoreThanOneItem.ElementAtOrNoneAsync(-1));
+ FunctionalAssert.Some(await EnumerableWithMoreThanOneItem.ElementAtOrNoneAsync(0));
+ FunctionalAssert.Some(await EnumerableWithMoreThanOneItem.ElementAtOrNoneAsync(1));
+ FunctionalAssert.Some(await EnumerableWithMoreThanOneItem.ElementAtOrNoneAsync(2));
+ FunctionalAssert.None(await EnumerableWithMoreThanOneItem.ElementAtOrNoneAsync(3));
+ FunctionalAssert.None(await EnumerableWithMoreThanOneItem.ElementAtOrNoneAsync(5));
+ FunctionalAssert.None(await EnumerableWithMoreThanOneItem.ElementAtOrNoneAsync(10));
+ }
+
+ public sealed class IndexIndex
+ {
+ [FunckyProperty(Verbose = true)]
+ public Property BehavesIdenticalToSynchronousCounterpart(List source, Index index)
+ => (source.ElementAtOrNone(index) == source.ToAsyncEnumerable().ElementAtOrNoneAsync(index).Result)
+ .ToProperty();
+ }
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/FirstOrNoneTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/FirstOrNoneTest.cs
new file mode 100644
index 000000000..6bbbd09ac
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/FirstOrNoneTest.cs
@@ -0,0 +1,43 @@
+#if INTEGRATED_ASYNC
+using static Funcky.Async.Test.Extensions.AsyncEnumerableExtensions.TestData;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class FirstOrNoneTest
+{
+ [Fact]
+ public async Task FirstOrNoneReturnsNoneWhenEnumerableIsEmpty()
+ {
+ FunctionalAssert.None(await EmptyEnumerable.FirstOrNoneAsync());
+ }
+
+ [Fact]
+ public async Task FirstOrNoneReturnsItemWhenEnumerableHasOneItem()
+ {
+ FunctionalAssert.Some(
+ FirstItem,
+ await EnumerableWithOneItem.FirstOrNoneAsync());
+ }
+
+ [Fact]
+ public async Task FirstOrNoneReturnsNoneWhenEnumerableHasOneItemButItDoesNotMatchPredicate()
+ {
+ FunctionalAssert.None(
+ await EnumerableWithOneItem.FirstOrNoneAsync(False));
+ }
+
+ [Fact]
+ public async Task FirstOrNoneReturnsItemWhenEnumerableHasMoreThanOneItem()
+ {
+ FunctionalAssert.Some(
+ FirstItem,
+ await EnumerableWithMoreThanOneItem.FirstOrNoneAsync());
+ }
+
+ [Fact]
+ public async Task FirstOrNoneReturnsNoneWhenEnumerableHasItemsButNoneMatchesPredicate()
+ {
+ FunctionalAssert.None(await EnumerableWithMoreThanOneItem.FirstOrNoneAsync(False));
+ }
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/InspectEmptyTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/InspectEmptyTest.cs
new file mode 100644
index 000000000..278b3a6af
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/InspectEmptyTest.cs
@@ -0,0 +1,37 @@
+#if INTEGRATED_ASYNC
+using Funcky.Async.Test.TestUtilities;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class InspectEmptyTest
+{
+ [Fact]
+ public void InspectEmptyIsEnumeratedLazily()
+ {
+ var doNotEnumerate = new FailOnEnumerateAsyncSequence();
+ _ = doNotEnumerate.InspectEmpty(NoOperation);
+ }
+
+ [Fact]
+ public async Task InspectEmptyExecutesAnInspectionFunctionOnMaterializationOnAnEmptyEnumerable()
+ {
+ var sideEffect = 0;
+ var asyncEnumerable = AsyncEnumerable.Empty();
+
+ _ = await asyncEnumerable.InspectEmpty(() => sideEffect = 1).MaterializeAsync();
+
+ Assert.Equal(1, sideEffect);
+ }
+
+ [Fact]
+ public void InspectEmptyExecutesNoInspectionFunctionOnMaterializationOnANonEmptyEnumerable()
+ {
+ var sideEffect = 0;
+ var asyncEnumerable = AsyncSequence.Return("Hello", "World");
+
+ _ = asyncEnumerable.InspectEmpty(() => sideEffect = 1).MaterializeAsync();
+
+ Assert.Equal(0, sideEffect);
+ }
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/InspectTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/InspectTest.cs
new file mode 100644
index 000000000..efa8a81a4
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/InspectTest.cs
@@ -0,0 +1,74 @@
+#if INTEGRATED_ASYNC
+using Funcky.Async.Test.TestUtilities;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class InspectTest
+{
+ [Fact]
+ public void InspectIsEnumeratedLazily()
+ {
+ var doNotEnumerate = new FailOnEnumerateAsyncSequence();
+
+ _ = doNotEnumerate.Inspect(NoOperation);
+ }
+
+ [Fact]
+ public void InspectAwaitIsEnumeratedLazily()
+ {
+ var doNotEnumerate = new FailOnEnumerateAsyncSequence();
+
+ _ = doNotEnumerate.InspectAwait(static _ => ValueTask.CompletedTask);
+ }
+
+ [Fact]
+ public async Task GivenAnAsyncEnumerableAndInjectWeCanApplySideEffectsToAsyncEnumerables()
+ {
+ var sideEffect = 0;
+ var numbers = AsyncSequence.Return(1, 2, 3, 42);
+
+ var numbersWithSideEffect = numbers
+ .Inspect(n => { ++sideEffect; });
+
+ Assert.Equal(0, sideEffect);
+
+ await numbersWithSideEffect.ToListAsync();
+
+ Assert.Equal(await numbers.CountAsync(), sideEffect);
+ }
+
+ [Fact]
+ public async Task GivenAnAsyncEnumerableAndInjectAnAsynchronouseActionWeCanApplySideEffectsToAsyncEnumerables()
+ {
+ var sideEffect = 0;
+ var numbers = AsyncSequence.Return(1, 2, 3, 42);
+
+ var numbersWithSideEffect = numbers
+ .InspectAwait(_ =>
+ {
+ ++sideEffect;
+ return default;
+ });
+
+ Assert.Equal(0, sideEffect);
+
+ await numbersWithSideEffect.ToListAsync();
+
+ Assert.Equal(await numbers.CountAsync(), sideEffect);
+ }
+
+ [Fact]
+ public async Task CancellationIsPropagated()
+ {
+ var canceledToken = new CancellationToken(canceled: true);
+ _ = await new AssertIsCancellationRequestedAsyncSequence().Inspect(NoOperation).ToListAsync(canceledToken);
+ }
+
+ [Fact]
+ public async Task CancellationIsPropagatedInAwaitOverload()
+ {
+ var canceledToken = new CancellationToken(canceled: true);
+ _ = await new AssertIsCancellationRequestedAsyncSequence().InspectAwait(_ => ValueTask.CompletedTask).ToListAsync(canceledToken);
+ }
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/InterleaveTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/InterleaveTest.cs
new file mode 100644
index 000000000..9dbbb5ea8
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/InterleaveTest.cs
@@ -0,0 +1,101 @@
+#if INTEGRATED_ASYNC
+using Funcky.Async.Test.TestUtilities;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class InterleaveTest
+{
+ [Fact]
+ public void InterleaveIsEnumeratedLazily()
+ {
+ var doNotEnumerate = new FailOnEnumerateAsyncSequence();
+
+ _ = doNotEnumerate.Interleave();
+ }
+
+ [Fact]
+ public async Task GivenAnEmptySequenceOfSequencesInterleaveReturnsAnEmptySequence()
+ {
+ var emptySequence = Enumerable.Empty>();
+
+ var interleaved = emptySequence.Interleave();
+
+ await AsyncAssert.Empty(interleaved);
+ }
+
+ [Fact]
+ public async Task GivenTwoSequencesOfEqualLengthIGetAnInterleavedResult()
+ {
+ var odds = AsyncSequence.Return(1, 3, 5, 7, 9, 11);
+ var evens = AsyncSequence.Return(2, 4, 6, 8, 10, 12);
+ var expected = AsyncSequence.Return(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
+
+ var interleaved = odds.Interleave(evens);
+
+ await AsyncAssert.Equal(expected, interleaved);
+ }
+
+ [Fact]
+ public async Task GivenTwoSequencesOfUnequalLengthIGetAnInterleavedResult()
+ {
+ var odds = AsyncSequence.Return(1, 3, 5, 7, 9, 11);
+ var evens = AsyncSequence.Return(2, 4, 6);
+ var expected = AsyncSequence.Return(1, 2, 3, 4, 5, 6, 7, 9, 11);
+
+ var interleaved = odds.Interleave(evens);
+
+ await AsyncAssert.Equal(expected, interleaved);
+ }
+
+ [Theory]
+ [InlineData("a", "b", "c")]
+ [InlineData("a", "c", "b")]
+ [InlineData("b", "a", "c")]
+ [InlineData("b", "c", "a")]
+ [InlineData("c", "a", "b")]
+ [InlineData("c", "b", "a")]
+ public async Task GivenMultipleSequencesTheOrderIsPreserved(string first, string second, string third)
+ {
+ var one = AsyncSequence.Return(first);
+ var two = AsyncSequence.Return(second);
+ var three = AsyncSequence.Return(third);
+
+ var interleaved = one.Interleave(two, three);
+
+ await AsyncAssert.Equal(AsyncSequence.Return(first, second, third), interleaved);
+ }
+
+ [Fact]
+ public async Task GivenASequenceOfSequenceTheInnerSequencesGetInterleaved()
+ {
+ var sequences = Sequence.Return(AsyncEnumerable.Repeat(1, 2), AsyncEnumerable.Repeat(42, 2));
+
+ await AsyncAssert.Equal(AsyncSequence.Return(1, 42, 1, 42), sequences.Interleave());
+ }
+
+ [Fact]
+ public async Task GivenOneSequenceWithElementsAndAllTheOtherSequencesEmptyWeGetTheFirstSequence()
+ {
+ var sequence = AsyncSequence.Return(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
+ var emptySequence = AsyncEnumerable.Empty();
+
+ await AsyncAssert.Equal(sequence, emptySequence.Interleave(emptySequence, sequence, emptySequence));
+ }
+
+ [Fact]
+ public async Task GivenASequenceOfSequencesInterleaveReturnsTheExpectedSequence()
+ {
+ var sequences = Sequence.Return(AsyncEnumerable.Repeat(1, 10), AsyncEnumerable.Repeat(2, 10), AsyncEnumerable.Repeat(3, 10), AsyncEnumerable.Repeat(4, 10));
+
+ var innerSum = sequences.Select(async element => await element.CountAsync()).Aggregate(0, (total, part) => total + part.Result);
+ Assert.Equal(innerSum, await sequences.Interleave().CountAsync());
+
+ int expected = 1;
+ await foreach (var element in sequences.Interleave())
+ {
+ Assert.Equal(expected, element);
+ expected = (expected % 4) + 1;
+ }
+ }
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/IntersperseTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/IntersperseTest.cs
new file mode 100644
index 000000000..67cfd753c
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/IntersperseTest.cs
@@ -0,0 +1,43 @@
+#if INTEGRATED_ASYNC
+using Funcky.Async.Test.TestUtilities;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class IntersperseTest
+{
+ [Fact]
+ public void IntersperseIsEvaluatedLazily()
+ {
+ var doNotEnumerate = new FailOnEnumerateAsyncSequence();
+ _ = doNotEnumerate.Intersperse(42);
+ }
+
+ [Fact]
+ public async Task InterspersingAnEmptyEnumerableReturnsAnEmptyEnumerable()
+ {
+ Assert.False(await AsyncEnumerable.Empty().Intersperse(42).AnyAsync());
+ }
+
+ [Fact]
+ public async Task InterspersingASequenceWithOneElementReturnsOriginalSequence()
+ {
+ var source = AsyncSequence.Return(10);
+ Assert.True(await source.SequenceEqualAsync(source.Intersperse(42)));
+ }
+
+ [Theory]
+ [MemberData(nameof(ValueReferenceEnumerables))]
+ public async Task InterspersingASequenceWithMoreThanOneElementReturnsExpectedSequence(IAsyncEnumerable expected, IAsyncEnumerable source)
+ {
+ Assert.True(await expected.SequenceEqualAsync(source.Intersperse(0)));
+ }
+
+ public static TheoryData, IAsyncEnumerable> ValueReferenceEnumerables()
+ => new()
+ {
+ { AsyncSequence.Return(1, 0, 2), AsyncSequence.Return(1, 2) },
+ { AsyncSequence.Return(1, 0, 2, 0, 3), AsyncSequence.Return(1, 2, 3) },
+ { AsyncSequence.Return(1, 0, 2, 0, 3, 0, 4), AsyncSequence.Return(1, 2, 3, 4) },
+ };
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/JoinToStringTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/JoinToStringTest.cs
new file mode 100644
index 000000000..09a72d344
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/JoinToStringTest.cs
@@ -0,0 +1,53 @@
+#if INTEGRATED_ASYNC
+// ReSharper disable PossibleMultipleEnumeration
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class JoinToStringTest
+{
+ [Fact]
+ public async Task JoiningAnEmptySetOfStringsReturnsAnEmptyString()
+ {
+ var empty = AsyncEnumerable.Empty();
+
+ Assert.Equal(string.Empty, await empty.JoinToStringAsync(", "));
+ Assert.Equal(string.Empty, await empty.JoinToStringAsync(','));
+ }
+
+ [Fact]
+ public async Task JoiningASetWithExactlyOneElementReturnsTheElementWithoutASeparator()
+ {
+ var singleElement = AsyncSequence.Return("Alpha");
+
+ Assert.Equal("Alpha", await singleElement.JoinToStringAsync(", "));
+ Assert.Equal("Alpha", await singleElement.JoinToStringAsync(','));
+ }
+
+ [Fact]
+ public async Task JoiningAListOfStringsAddsSeparatorsBetweenTheElements()
+ {
+ var strings = AsyncSequence.Return("Alpha", "Beta", "Gamma");
+
+ Assert.Equal("Alpha, Beta, Gamma", await strings.JoinToStringAsync(", "));
+ Assert.Equal("Alpha,Beta,Gamma", await strings.JoinToStringAsync(','));
+ }
+
+ [Fact]
+ public async Task JoiningNonStringsReturnASeparatedListToo()
+ {
+ var numbers = AsyncSequence.Return(1, 2, 3);
+
+ Assert.Equal("1, 2, 3", await numbers.JoinToStringAsync(", "));
+ Assert.Equal("1,2,3", await numbers.JoinToStringAsync(','));
+ }
+
+ [Fact]
+ public async Task NullsAreHandledAsEmptyStringsWhileJoining()
+ {
+ var strings = AsyncSequence.Return("Alpha", null, "Gamma");
+
+ Assert.Equal("Alpha, , Gamma", await strings.JoinToStringAsync(", "));
+ Assert.Equal("Alpha,,Gamma", await strings.JoinToStringAsync(','));
+ }
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/LastOrNoneTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/LastOrNoneTest.cs
new file mode 100644
index 000000000..aa98db09c
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/LastOrNoneTest.cs
@@ -0,0 +1,43 @@
+#if INTEGRATED_ASYNC
+using static Funcky.Async.Test.Extensions.AsyncEnumerableExtensions.TestData;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class LastOrNoneTest
+{
+ [Fact]
+ public async Task LastOrNoneReturnsNoneWhenEnumerableIsEmpty()
+ {
+ FunctionalAssert.None(await EmptyEnumerable.LastOrNoneAsync());
+ }
+
+ [Fact]
+ public async Task LastOrNoneReturnsItemWhenEnumerableHasOneItem()
+ {
+ FunctionalAssert.Some(
+ FirstItem,
+ await EnumerableWithOneItem.LastOrNoneAsync());
+ }
+
+ [Fact]
+ public async Task LastOrNoneReturnsNoneWhenEnumerableHasOneItemButItDoesNotMatchPredicate()
+ {
+ FunctionalAssert.None(
+ await EnumerableWithOneItem.LastOrNoneAsync(False));
+ }
+
+ [Fact]
+ public async Task LastOrNoneReturnsLastItemWhenEnumerableHasMoreThanOneItem()
+ {
+ FunctionalAssert.Some(
+ LastItem,
+ await EnumerableWithMoreThanOneItem.LastOrNoneAsync());
+ }
+
+ [Fact]
+ public async Task LastOrNoneReturnsNoneWhenEnumerableHasItemsButNoneMatchesPredicate()
+ {
+ FunctionalAssert.None(await EnumerableWithMoreThanOneItem.LastOrNoneAsync(False));
+ }
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/MaterializeTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/MaterializeTest.cs
new file mode 100644
index 000000000..a4b2c086f
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/MaterializeTest.cs
@@ -0,0 +1,36 @@
+#if INTEGRATED_ASYNC
+using Funcky.Async.Test.TestUtilities;
+using Xunit.Sdk;
+
+namespace Funcky.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class MaterializeTest
+{
+ [Fact]
+ public async Task MaterializeEnumeratesNonCollection()
+ {
+ var doNotEnumerate = new FailOnEnumerateAsyncSequence();
+
+ await Assert.ThrowsAsync(async () => await doNotEnumerate.MaterializeAsync());
+ }
+
+ [Fact]
+ public async Task MaterializeASequenceReturnsAListByDefault()
+ {
+ var sequence = AsyncEnumerable.Repeat("Hello world!", 3);
+
+ Assert.IsType>(await sequence.MaterializeAsync());
+ }
+
+ [Fact]
+ public async Task MaterializeWithMaterializationReturnsCorrectCollectionWhenEnumerate()
+ {
+ var sequence = AsyncEnumerable.Repeat("Hello world!", 3);
+
+ Assert.IsType>(await sequence.MaterializeAsync(ToHashSet));
+ }
+
+ private static ValueTask> ToHashSet(IAsyncEnumerable sequence, CancellationToken cancellationToken)
+ => sequence.ToHashSetAsync(null, cancellationToken);
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/MaxOrNoneTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/MaxOrNoneTest.cs
new file mode 100644
index 000000000..e0c4a76be
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/MaxOrNoneTest.cs
@@ -0,0 +1,222 @@
+#if INTEGRATED_ASYNC
+// ReSharper disable PossibleMultipleEnumeration
+using FsCheck;
+using FsCheck.Fluent;
+using Funcky.Test.Internal;
+using Funcky.Test.Internal.Data;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class MaxOrNoneTest
+{
+ // Int32/int Tests
+ [FunckyAsyncProperty]
+ public Property MaxOrNoneAsyncGivesTheSameResultAsMaxAsyncForInt32(IAsyncEnumerable sequence)
+ => CompareMaxAndHandleEmptyInt32Sequence(sequence).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property MaxOrNoneAsyncGivesTheSameResultAsMaxForNullableAsyncForInt32(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.MaxAsync().Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxOrNoneAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForInt32(IAsyncEnumerable sequence, Func selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAsync(SelectorTransformation.TransformNullableSelector(selector)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxOrNoneAwaitAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForInt32(IAsyncEnumerable sequence, AwaitSelector selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector.Get).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAwaitAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxAwaitWithCancellationAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForInt32(IAsyncEnumerable sequence, AwaitSelectorWithCancellation selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector.Get).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAwaitWithCancellationAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ // Int64/long Tests
+ [FunckyAsyncProperty]
+ public Property MaxOrNoneAsyncGivesTheSameResultAsMaxAsyncForInt64(IAsyncEnumerable sequence)
+ => CompareMaxAndHandleEmptyInt64Sequence(sequence).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property MaxOrNoneAsyncGivesTheSameResultAsMaxForNullableAsyncForInt64(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.MaxAsync().Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxOrNoneAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForInt64(IAsyncEnumerable sequence, Func selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAsync(SelectorTransformation.TransformNullableSelector(selector)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxOrNoneAwaitAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForInt64(IAsyncEnumerable sequence, AwaitSelector selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector.Get).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAwaitAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxAwaitWithCancellationAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForInt64(IAsyncEnumerable sequence, AwaitSelectorWithCancellation selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector.Get).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAwaitWithCancellationAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ // Single/float Tests
+ [FunckyAsyncProperty]
+ public Property MaxOrNoneAsyncGivesTheSameResultAsMaxAsyncForSingle(IAsyncEnumerable sequence)
+ => CompareMaxAndHandleEmptySingleSequence(sequence).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property MaxOrNoneAsyncGivesTheSameResultAsMaxForNullableAsyncForSingle(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.MaxAsync().Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxOrNoneAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForSingle(IAsyncEnumerable sequence, Func selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAsync(SelectorTransformation.TransformNullableSelector(selector)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxOrNoneAwaitAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForSingle(IAsyncEnumerable sequence, AwaitSelector selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector.Get).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAwaitAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxAwaitWithCancellationAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForSingle(IAsyncEnumerable sequence, AwaitSelectorWithCancellation selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector.Get).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAwaitWithCancellationAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ // Double/double Tests
+ [FunckyAsyncProperty]
+ public Property MaxOrNoneAsyncGivesTheSameResultAsMaxAsyncForDouble(IAsyncEnumerable sequence)
+ => CompareMaxAndHandleEmptyDoubleSequence(sequence).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property MaxOrNoneAsyncGivesTheSameResultAsMaxForNullableAsyncForDouble(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.MaxAsync().Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxOrNoneAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForDouble(IAsyncEnumerable sequence, Func selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAsync(SelectorTransformation.TransformNullableSelector(selector)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxOrNoneAwaitAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForDouble(IAsyncEnumerable sequence, AwaitSelector selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector.Get).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAwaitAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxAwaitWithCancellationAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForDouble(IAsyncEnumerable sequence, AwaitSelectorWithCancellation selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector.Get).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAwaitWithCancellationAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ // Decimal/decimal Tests
+ [FunckyAsyncProperty]
+ public Property MaxOrNoneAsyncGivesTheSameResultAsMaxAsyncForDecimal(IAsyncEnumerable sequence)
+ => CompareMaxAndHandleEmptyDecimalSequence(sequence).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property MaxOrNoneAsyncGivesTheSameResultAsMaxForNullableAsyncForDecimal(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.MaxAsync().Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxOrNoneAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForDecimal(IAsyncEnumerable sequence, Func selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAsync(SelectorTransformation.TransformNullableSelector(selector)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxOrNoneAwaitAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForDecimal(IAsyncEnumerable sequence, AwaitSelector selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector.Get).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAwaitAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxAwaitWithCancellationAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForDecimal(IAsyncEnumerable sequence, AwaitSelectorWithCancellation selector)
+ => (Option.FromNullable(sequence.MaxByAsync(selector.Get).Result)
+ == sequence.Select(Option.FromNullable).MaxOrNoneAwaitWithCancellationAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ // Generic TSource implementing IComparable Tests
+ [FunckyAsyncProperty]
+ public Property MaxOrNoneGivesTheSameResultAsMaxForAnyIComparable(IAsyncEnumerable sequence)
+ => CompareMaxAndHandleEmptyPersonSequence(sequence.Select(Person.Create)).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property MaxOrNoneAsyncGivesTheSameResultAsMaxForNullableAsyncForAnyIComparable(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.Select(Person.Create).MaxAsync().Result)
+ == sequence.Select(Person.Create).Select(Option.FromNullable).MaxOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxOrNoneAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForAnyIComparable(IAsyncEnumerable sequence, Func selector)
+ => (Option.FromNullable(sequence.Select(Person.Create).MaxByAsync(SelectorTransformation.TransformPersonSelector(selector)).Result)
+ == sequence.Select(Person.Create).Select(Option.FromNullable).MaxOrNoneAsync(SelectorTransformation.TransformOptionPersonSelector(selector)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxOrNoneAwaitAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForAnyIComparable(IAsyncEnumerable sequence, AwaitSelector selector)
+ => (Option.FromNullable(sequence.Select(Person.Create).MaxByAsync(SelectorTransformation.TransformPersonSelector(selector.Get)).Result)
+ == sequence.Select(Person.Create).Select(Option.FromNullable).MaxOrNoneAwaitAsync(SelectorTransformation.TransformOptionPersonSelector(selector.Get)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MaxAwaitWithCancellationAsyncWithSelectorGivesTheSameResultAsMaxForNullableAsyncForAnyIComparable(IAsyncEnumerable sequence, AwaitSelectorWithCancellation selector)
+ {
+ var result1 = Option.FromNullable(sequence.Select(Person.Create)
+ .MaxByAsync(SelectorTransformation.TransformPersonSelector(selector.Get)).Result);
+
+ var result2 = sequence.Select(Person.Create).Select(Option.FromNullable)
+ .MaxOrNoneAwaitWithCancellationAsync(SelectorTransformation.TransformOptionPersonSelector(selector.Get)).Result;
+
+ return (result1 == result2).ToProperty();
+ }
+
+ [Fact]
+ public void Failing()
+ {
+ var sequence = new List { -1, -1, 1 };
+
+ var min = Option.FromNullable(sequence.Select(Person.Create).Max());
+ var minOrNone = sequence.Select(Person.Create).Select(Option.FromNullable).MaxOrNone();
+
+ Assert.True(min == minOrNone);
+ Assert.Equal(0, min.CompareTo(minOrNone));
+ Assert.Equal(min, minOrNone);
+ }
+
+ [Fact]
+ public void Confused()
+ {
+ Person personA = new(42);
+ Person personB = new(42);
+
+ Assert.Equal(personA, personB);
+ Assert.Equal(Option.FromNullable(personA), Option.FromNullable(personB));
+ }
+
+ private static async Task CompareMaxAndHandleEmptyInt32Sequence(IAsyncEnumerable sequence)
+ => await sequence.AnyAsync()
+ ? await sequence.MaxAsync() == await sequence.MaxOrNoneAsync()
+ : (await sequence.MaxOrNoneAsync()).Match(none: true, some: _ => false);
+
+ private static async Task CompareMaxAndHandleEmptyInt64Sequence(IAsyncEnumerable sequence)
+ => await sequence.AnyAsync()
+ ? await sequence.MaxAsync() == await sequence.MaxOrNoneAsync()
+ : (await sequence.MaxOrNoneAsync()).Match(none: true, some: _ => false);
+
+ private static async Task CompareMaxAndHandleEmptySingleSequence(IAsyncEnumerable sequence)
+ => await sequence.AnyAsync()
+ ? await sequence.MaxAsync() == await sequence.MaxOrNoneAsync()
+ : (await sequence.MaxOrNoneAsync()).Match(none: true, some: _ => false);
+
+ private static async Task CompareMaxAndHandleEmptyDoubleSequence(IAsyncEnumerable sequence)
+ => await sequence.AnyAsync()
+ ? await sequence.MaxAsync() == await sequence.MaxOrNoneAsync()
+ : (await sequence.MaxOrNoneAsync()).Match(none: true, some: _ => false);
+
+ private static async Task CompareMaxAndHandleEmptyDecimalSequence(IAsyncEnumerable sequence)
+ => await sequence.AnyAsync()
+ ? await sequence.MaxAsync() == await sequence.MaxOrNoneAsync()
+ : (await sequence.MaxOrNoneAsync()).Match(none: true, some: _ => false);
+
+ private static async Task CompareMaxAndHandleEmptyPersonSequence(IAsyncEnumerable sequence)
+ => await sequence.AnyAsync()
+ ? (await sequence.MaxOrNoneAsync()).Match(none: false, some: p => p.CompareTo(sequence.MaxAsync().Result) == 0)
+ : (await sequence.MaxOrNoneAsync()).Match(none: true, some: _ => false);
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/MemoizeTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/MemoizeTest.cs
new file mode 100644
index 000000000..7b3fd8115
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/MemoizeTest.cs
@@ -0,0 +1,111 @@
+#if INTEGRATED_ASYNC
+using Funcky.Async.Test.TestUtilities;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class MemoizeTest
+{
+ [Fact]
+ public async Task MemoizeIsEnumeratedLazily()
+ {
+ var doNotEnumerate = new FailOnEnumerateAsyncSequence();
+
+ await using var memoized = doNotEnumerate.Memoize();
+ }
+
+ [Fact]
+ public async Task TheUnderlyingAsyncEnumerableIsOnlyEnumeratedOnce()
+ {
+ var enumerateOnce = AsyncEnumerateOnce.Create(Sequence.Return("Alpha", "Beta"));
+ await using var memoized = enumerateOnce.Memoize();
+
+ Assert.Equal("Alpha", await memoized.FirstAsync());
+ Assert.Equal("Alpha", await memoized.FirstAsync());
+
+ Assert.Equal("Beta", await memoized.LastAsync());
+ Assert.Equal("Beta", await memoized.LastAsync());
+ }
+
+ [Fact]
+ public async Task MemoizingAnEmptyListIsEmptyAsync()
+ {
+ var empty = AsyncEnumerable.Empty();
+ await using var memoized = empty.Memoize();
+
+ await AsyncAssert.Empty(memoized);
+ }
+
+ [Fact]
+ public async Task OutOfOrderMoveNextReturnsItemsInTheRightOrderAsync()
+ {
+ await using var memoizedRange = AsyncEnumerable.Range(1, 10).Memoize();
+
+ await using var enumerator1 = memoizedRange.GetAsyncEnumerator();
+
+ Assert.True(await enumerator1.MoveNextAsync());
+ Assert.True(await enumerator1.MoveNextAsync());
+
+ Assert.Equal(2, enumerator1.Current);
+
+ await using var enumerator2 = memoizedRange.GetAsyncEnumerator();
+
+ Assert.True(await enumerator2.MoveNextAsync());
+ Assert.Equal(1, enumerator2.Current);
+
+ Assert.True(await enumerator1.MoveNextAsync());
+ Assert.True(await enumerator1.MoveNextAsync());
+
+ Assert.Equal(4, enumerator1.Current);
+
+ Assert.True(await enumerator2.MoveNextAsync());
+ Assert.Equal(2, enumerator2.Current);
+ Assert.True(await enumerator2.MoveNextAsync());
+ Assert.Equal(3, enumerator2.Current);
+ Assert.True(await enumerator2.MoveNextAsync());
+ Assert.Equal(4, enumerator2.Current);
+ Assert.True(await enumerator2.MoveNextAsync());
+ Assert.Equal(5, enumerator2.Current);
+ }
+
+ [Fact]
+ public async Task DisposingAMemoizedBufferDoesNotDisposeOriginalBuffer()
+ {
+ var source = AsyncEnumerateOnce.Create(Enumerable.Empty());
+ await using var firstMemoization = source.Memoize();
+
+ await using (firstMemoization.Memoize())
+ {
+ }
+
+ await firstMemoization.ToListAsync();
+ }
+
+ [Fact]
+ public async Task DisposingAMemoizedBorrowedBufferDoesNotDisposeOriginalBorrowedBuffer()
+ {
+ var source = AsyncEnumerateOnce.Create([]);
+ await using var firstMemoization = source.Memoize();
+ await using var borrowedBuffer = firstMemoization.Memoize();
+
+ await using (borrowedBuffer.Memoize())
+ {
+ }
+
+ await borrowedBuffer.ToListAsync();
+ }
+
+ /// This test disallows "re-borrowing" i.e. creating a fresh BorrowedBuffer over the original buffer.
+ [Fact]
+ public async Task UsagesOfSecondBorrowThrowAfterFirstBorrowIsDisposed()
+ {
+ var source = AsyncEnumerateOnce.Create([]);
+ await using var firstMemoization = source.Memoize();
+ await using var firstBorrow = firstMemoization.Memoize();
+ await using var secondBorrow = firstBorrow.Memoize();
+#pragma warning disable IDISP017
+ await firstBorrow.DisposeAsync();
+#pragma warning restore IDISP017
+ await Assert.ThrowsAsync(async () => await secondBorrow.ToListAsync());
+ }
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/MergeTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/MergeTest.cs
new file mode 100644
index 000000000..1daee8d53
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/MergeTest.cs
@@ -0,0 +1,80 @@
+#if INTEGRATED_ASYNC
+// ReSharper disable PossibleMultipleEnumeration
+
+using System.Collections.Immutable;
+using FsCheck.Xunit;
+using Funcky.Async.Test.TestUtilities;
+using Funcky.Test.Internal;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class MergeTest
+{
+ [Fact]
+ public void MergeIsEnumeratedLazily()
+ {
+ var doNotEnumerate = new FailOnEnumerateAsyncSequence();
+
+ _ = doNotEnumerate.Merge(doNotEnumerate);
+ }
+
+ [Fact]
+ public Task MergeEmptySequencesResultsInAnEmptySequence()
+ {
+ var emptySequence = AsyncEnumerable.Empty();
+
+ return AsyncAssert.Empty(emptySequence.Merge(emptySequence, emptySequence, emptySequence));
+ }
+
+ [Fact]
+ public async Task MergeAnEmptySequenceWithANonEmptySequenceResultsInTheNonEmptySequenceAsync()
+ {
+ var nonEmptySequence = AsyncSequence.Return(1, 2, 4, 7);
+ var emptySequence = AsyncEnumerable.Empty();
+
+ await AsyncAssert.Equal(nonEmptySequence, nonEmptySequence.Merge(emptySequence));
+ await AsyncAssert.Equal(nonEmptySequence, emptySequence.Merge(nonEmptySequence));
+ }
+
+ [Property]
+ public void TwoSingleSequencesAreMergedCorrectlyAsync(int first, int second)
+ {
+ var sequence1 = AsyncSequence.Return(first);
+ var sequence2 = AsyncSequence.Return(second);
+
+ var merged = sequence1.Merge(sequence2);
+ Assert.True(merged.FirstAsync().Result <= merged.LastAsync().Result);
+ }
+
+ [Fact]
+ public Task MergeTwoSequencesToOneAsync()
+ {
+ var sequence1 = AsyncSequence.Return(1, 2, 4, 7);
+ var sequence2 = AsyncSequence.Return(3, 5, 6, 8);
+ var expected = AsyncEnumerable.Range(1, 8);
+
+ return AsyncAssert.Equal(expected, sequence1.Merge(sequence2));
+ }
+
+ [Fact]
+ public Task MergeASequenceOfSequences()
+ {
+ var sequence1 = AsyncSequence.Return(1, 2, 4, 7);
+ var sequence2 = AsyncSequence.Return(3, 5, 6, 8);
+ var mergeable = ImmutableList>.Empty.Add(sequence1).Add(sequence2);
+ var expected = AsyncEnumerable.Range(1, 8);
+
+ return AsyncAssert.Equal(expected, mergeable.Merge());
+ }
+
+ [Fact]
+ public Task MergeASequenceWithADifferentComparer()
+ {
+ var sequence1 = AsyncSequence.Return(7, 4, 2, 1);
+ var sequence2 = AsyncSequence.Return(8, 6, 5, 3);
+ var expected = AsyncEnumerable.Range(1, 8).Reverse();
+
+ return AsyncAssert.Equal(expected, sequence1.Merge(sequence2, DescendingIntComparer.Create()));
+ }
+}
+#endif
diff --git a/Funcky.Test/Extensions/AsyncEnumerableExtensions/MinOrNoneTest.cs b/Funcky.Test/Extensions/AsyncEnumerableExtensions/MinOrNoneTest.cs
new file mode 100644
index 000000000..35400a856
--- /dev/null
+++ b/Funcky.Test/Extensions/AsyncEnumerableExtensions/MinOrNoneTest.cs
@@ -0,0 +1,215 @@
+#if INTEGRATED_ASYNC
+// ReSharper disable PossibleMultipleEnumeration
+using FsCheck;
+using FsCheck.Fluent;
+using Funcky.Test.Internal;
+using Funcky.Test.Internal.Data;
+
+namespace Funcky.Async.Test.Extensions.AsyncEnumerableExtensions;
+
+public sealed class MinOrNoneTest
+{
+ // Int32/int Tests
+ [FunckyAsyncProperty]
+ public Property MinOrNoneAsyncGivesTheSameResultAsMinAsyncForInt32(IAsyncEnumerable sequence)
+ => CompareMinAndHandleEmptyInt32Sequence(sequence).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property MinOrNoneAsyncGivesTheSameResultAsMinForNullableAsyncForInt32(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.MinAsync().Result)
+ == sequence.Select(Option.FromNullable).MinOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MinOrNoneAsyncWithSelectorGivesTheSameResultAsMinForNullableAsyncForInt32(IAsyncEnumerable sequence, Func selector)
+ => (Option.FromNullable(sequence.MinByAsync(selector).Result)
+ == sequence.Select(Option.FromNullable).MinOrNoneAsync(SelectorTransformation.TransformNullableSelector(selector)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MinOrNoneAwaitAsyncWithSelectorGivesTheSameResultAsMinForNullableAsyncForInt32(IAsyncEnumerable sequence, AwaitSelector selector)
+ => (Option.FromNullable(sequence.MinByAsync(selector.Get).Result)
+ == sequence.Select(Option.FromNullable).MinOrNoneAwaitAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MinAwaitWithCancellationAsyncWithSelectorGivesTheSameResultAsMinForNullableAsyncForInt32(IAsyncEnumerable sequence, AwaitSelectorWithCancellation selector)
+ => (Option.FromNullable(sequence.MinByAsync(selector.Get).Result)
+ == sequence.Select(Option.FromNullable).MinOrNoneAwaitWithCancellationAsync(SelectorTransformation.TransformNullableSelector(selector.Get)).Result).ToProperty();
+
+ // Int64/long Tests
+ [FunckyAsyncProperty]
+ public Property MinOrNoneAsyncGivesTheSameResultAsMinAsyncForInt64(IAsyncEnumerable sequence)
+ => CompareMinAndHandleEmptyInt64Sequence(sequence).Result.ToProperty();
+
+ [FunckyAsyncProperty]
+ public Property MinOrNoneAsyncGivesTheSameResultAsMinForNullableAsyncForInt64(IAsyncEnumerable sequence)
+ => (Option.FromNullable(sequence.MinAsync().Result)
+ == sequence.Select(Option.FromNullable).MinOrNoneAsync().Result).ToProperty();
+
+ [FunckyAsyncProperty(Skip = "Tofix")]
+ public Property MinOrNoneAsyncWithSelectorGivesTheSameResultAsMinForNullableAsyncForInt64(IAsyncEnumerable sequence, Func