Skip to content

Commit

Permalink
Merge branch 'main' into generic-parse-or-none
Browse files Browse the repository at this point in the history
  • Loading branch information
FreeApophis authored Sep 22, 2023
2 parents 1651758 + 79d8f9b commit 0366510
Show file tree
Hide file tree
Showing 17 changed files with 307 additions and 112 deletions.
57 changes: 40 additions & 17 deletions Documentation/src/enumerable-extensions/cartesian-product.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,61 @@
## CartesianProduct

In mathematics, specifically set theory, the Cartesian product of two sets A and B, denoted A×B, is the set of all ordered pairs (a, b) where a is in A and b is in B.
In mathematics, specifically set theory, the Cartesian product of two sets A and B, denoted A×B,
is the set of all ordered pairs (a, b) where a ∈ A and b ∈ B.

The CartesianProduct extension function returns all possible pairs of two given IEnumerables.

There are two overloads, one which lets you choose
In other words: The Cartesian product produces all possible pairs of two given `IEnumerable`s.

![cartesian-product with marbles](cartesian-product.svg "Marble me!")

### Recipe
The Cartesian product can be easily implemented ad-hoc using LINQ's built-in `SelectMany` extension function:

```cs
using System;
using System.Linq;

// Version A: Get each pair as a tuple
var result = sequenceA.SelectMany(_ => sequenceB, ValueTuple.Create);

// Version B: Transform each pair using a selector
var result = sequenceA.SelectMany(_ => sequenceB, (a, b) => ...);

// Version C: Using LINQs declarative query syntax
var result =
from a in sequenceA
from b in sequenceB
select ...;
```


### Examples

Two sequences as input:

```
```
smiles = [😀, 😐, 🙄]
fruits = [🍉, 🍌, 🍇, 🍓]
```
```

The cartesian products of smiles and fruits:
The Cartesian products of smiles and fruits:

```
smiles × fruits => [[😀, 🍉], [😀, 🍌], [😀, 🍇], [😀, 🍓],
[😐, 🍉], [😐, 🍌], [😐, 🍇], [😐, 🍓],
```
smiles × fruits => [[😀, 🍉], [😀, 🍌], [😀, 🍇], [😀, 🍓],
[😐, 🍉], [😐, 🍌], [😐, 🍇], [😐, 🍓],
[🙄, 🍉], [🙄, 🍌], [🙄, 🍇], [🙄, 🍓]]
```

In this C# example you see how all Playing cards are in fact a cartesian product of a suit and a value.
In this C# example you see how all playing cards are in fact a Cartesian products of a suit and a value.

This example uses the overload with our own selector, because we just want a sequence of strings.
This example uses the overload with a selector, because we just want a sequence of strings.

```cs
var suits = ImmutableList.Create("", "", "", "");
var values =
ImmutableList.Create("2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A");
using System;
using System.Linq;
using Funcky;

var suits = Sequence.Return("", "", "", "");
var values = Sequence.Return("2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A");

var allCards = suits.CartesianProduct(values, (suit, value) => $"{value}{suit}");
```
var deck = suits.SelectMany(_ => values, (suit, value) => $"{value}{suit}");
```
45 changes: 18 additions & 27 deletions Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/ChunkTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,29 +42,29 @@ public async Task GivenAnEnumerableWeChanChunkItIntoAnEnumerableOfEnumerablesAsy

await AsyncAssert.Collection(
chunked,
a =>
chunk =>
{
Assert.Collection(
a,
aa => Assert.Equal(1, aa),
ab => Assert.Equal(2, ab),
ac => Assert.Equal(3, ac));
chunk,
value => Assert.Equal(1, value),
value => Assert.Equal(2, value),
value => Assert.Equal(3, value));
},
b =>
chunk =>
{
Assert.Collection(
b,
ba => Assert.Equal(4, ba),
bb => Assert.Equal(5, bb),
bc => Assert.Equal(6, bc));
chunk,
value => Assert.Equal(4, value),
value => Assert.Equal(5, value),
value => Assert.Equal(6, value));
},
c =>
chunk =>
{
Assert.Collection(
c,
ca => Assert.Equal(7, ca),
cb => Assert.Equal(8, cb),
cc => Assert.Equal(9, cc));
chunk,
value => Assert.Equal(7, value),
value => Assert.Equal(8, value),
value => Assert.Equal(9, value));
});
}

Expand All @@ -79,18 +79,9 @@ public async Task GivenAnEnumerableNotAMultipleOfSizeWeHaveASmallerLastSlice()

await AsyncAssert.Collection(
chunked,
a =>
{
Assert.Equal(chunkSize, a.Count);
},
b =>
{
Assert.Equal(chunkSize, b.Count);
},
c =>
{
Assert.Equal(count % chunkSize, c.Count);
});
chunk => Assert.Equal(chunkSize, chunk.Count),
chunk => Assert.Equal(chunkSize, chunk.Count),
chunk => Assert.Equal(count % chunkSize, chunk.Count));
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public async Task GivenASourceSequenceShorterThanTheSlidingWindowWidthReturnsAnE
}

[Fact]
public async Task SlidingWindowReturnsTheCorrectAmountOfWindowsAllOfEvenSizeAsync()
public async Task SlidingWindowReturnsTheCorrectAmountOfWindowsAllOfTheSameSizeAsync()
{
const int width = 5;
var source = AsyncEnumerable.Range(0, 10);
Expand Down Expand Up @@ -74,9 +74,9 @@ public async Task SlidingWindowReturnsASequenceOfConsecutiveWindowsAsync()

await AsyncAssert.Collection(
source.SlidingWindow(width),
window1 => { Assert.Equal(Enumerable.Range(0, width), window1); },
window2 => { Assert.Equal(Enumerable.Range(1, width), window2); },
window3 => { Assert.Equal(Enumerable.Range(2, width), window3); });
window => Assert.Equal(Enumerable.Range(0, width), window),
window => Assert.Equal(Enumerable.Range(1, width), window),
window => Assert.Equal(Enumerable.Range(2, width), window));
}

[Fact]
Expand Down
14 changes: 5 additions & 9 deletions Funcky.Async/Extensions/AsyncEnumerableExtensions/Chunk.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using Funcky.Internal.Validators;

namespace Funcky.Extensions;

Expand All @@ -13,7 +14,7 @@ public static partial class AsyncEnumerableExtensions
/// <returns>A sequence of equally sized sequences containing elements of the source collection in the same order.</returns>
[Pure]
public static IAsyncEnumerable<IReadOnlyList<TSource>> Chunk<TSource>(this IAsyncEnumerable<TSource> source, int size)
=> ChunkEnumerable(source, ValidateChunkSize(size));
=> ChunkEnumerable(source, ChunkSizeValidator.Validate(size));

/// <summary>
/// Chunks the source sequence into equally sized chunks. The last chunk can be smaller.
Expand All @@ -26,7 +27,7 @@ public static IAsyncEnumerable<IReadOnlyList<TSource>> Chunk<TSource>(this IAsyn
/// <returns>A sequence of results based on equally sized chunks.</returns>
[Pure]
public static IAsyncEnumerable<TResult> Chunk<TSource, TResult>(this IAsyncEnumerable<TSource> source, int size, Func<IReadOnlyList<TSource>, TResult> resultSelector)
=> ChunkEnumerable(source, ValidateChunkSize(size))
=> ChunkEnumerable(source, ChunkSizeValidator.Validate(size))
.Select(resultSelector);

/// <summary>
Expand All @@ -40,7 +41,7 @@ public static IAsyncEnumerable<TResult> Chunk<TSource, TResult>(this IAsyncEnume
/// <returns>A sequence of results based on equally sized chunks.</returns>
[Pure]
public static IAsyncEnumerable<TResult> ChunkAwait<TSource, TResult>(this IAsyncEnumerable<TSource> source, int size, Func<IReadOnlyList<TSource>, ValueTask<TResult>> resultSelector)
=> ChunkEnumerable(source, ValidateChunkSize(size))
=> ChunkEnumerable(source, ChunkSizeValidator.Validate(size))
.SelectAwait(resultSelector);

/// <summary>
Expand All @@ -54,14 +55,9 @@ public static IAsyncEnumerable<TResult> ChunkAwait<TSource, TResult>(this IAsync
/// <returns>A sequence of results based on equally sized chunks.</returns>
[Pure]
public static IAsyncEnumerable<TResult> ChunkAwaitWithCancellation<TSource, TResult>(this IAsyncEnumerable<TSource> source, int size, Func<IReadOnlyList<TSource>, CancellationToken, ValueTask<TResult>> resultSelector)
=> ChunkEnumerable(source, ValidateChunkSize(size))
=> ChunkEnumerable(source, ChunkSizeValidator.Validate(size))
.SelectAwaitWithCancellation(resultSelector);

private static int ValidateChunkSize(int size)
=> size > 0
? size
: throw new ArgumentOutOfRangeException(nameof(size), size, "Size must be bigger than 0");

private static async IAsyncEnumerable<IReadOnlyList<TSource>> ChunkEnumerable<TSource>(IAsyncEnumerable<TSource> source, int size, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var asyncEnumerator = source.GetAsyncEnumerator(cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Runtime.CompilerServices;
using Funcky.Internal;
using Funcky.Internal.Validators;

namespace Funcky.Extensions;

Expand All @@ -19,7 +20,7 @@ public static partial class AsyncEnumerableExtensions
/// <returns>Returns a sequence of equally sized window sequences.</returns>
[Pure]
public static IAsyncEnumerable<IReadOnlyList<TSource>> SlidingWindow<TSource>(this IAsyncEnumerable<TSource> source, int width)
=> SlidingWindowEnumerable(source, ValidateWindowWidth(width));
=> SlidingWindowEnumerable(source, WindowWidthValidator.Validate(width));

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static async IAsyncEnumerable<IReadOnlyList<TSource>> SlidingWindowEnumerable<TSource>(
Expand All @@ -36,10 +37,4 @@ private static async IAsyncEnumerable<IReadOnlyList<TSource>> SlidingWindowEnume
}
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int ValidateWindowWidth(int width)
=> width > 0
? width
: throw new ArgumentOutOfRangeException(nameof(width), width, "The width of the window must be bigger than 0");
}
2 changes: 2 additions & 0 deletions Funcky.Async/Funcky.Async.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
<Compile Include="..\Funcky\Internal\Mixer.cs" Link="Internal\Mixer.cs" />
<Compile Include="..\Funcky\Internal\SlidingWindowQueue.cs" Link="Internal\SlidingWindowQueue.cs" />
<Compile Include="..\Funcky\Internal\UnsafeEither.cs" Link="Internal\UnsafeEither.cs" />
<Compile Include="..\Funcky\Internal\Validators\WindowWidthValidator.cs" Link="Internal\Validators\WindowWidthValidator.cs" />
<Compile Include="..\Funcky\Internal\Validators\ChunkSizeValidator.cs" Link="Internal\Validators\ChunkSizeValidator.cs" />
<Compile Include="..\Funcky\Compatibility\DynamicallyAccessedMemberTypes.cs" Link="Compatibility\DynamicallyAccessedMemberTypes.cs" />
<Compile Include="..\Funcky\Compatibility\DynamicallyAccessedMembersAttribute.cs" Link="Compatibility\DynamicallyAccessedMembersAttribute.cs" />
<Compile Include="..\Funcky\Internal\PartitionBuilder.cs" Link="Internal\PartitionBuilder.cs" />
Expand Down
51 changes: 21 additions & 30 deletions Funcky.Test/Extensions/EnumerableExtensions/ChunkTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ public void GivenAnSingleElementListWeGetEnumerableWithOneElement()

Assert.Collection(
chunked,
a =>
chunk =>
{
Assert.Collection(
a,
aa => Assert.Equal(1, aa));
chunk,
value => Assert.Equal(1, value));
});
}

Expand All @@ -48,29 +48,29 @@ public void GivenAnEnumerableWeChanChunkItIntoAnEnumerableOfEnumerables()

Assert.Collection(
chunked,
a =>
chunk =>
{
Assert.Collection(
a,
aa => Assert.Equal(1, aa),
ab => Assert.Equal(2, ab),
ac => Assert.Equal(3, ac));
chunk,
value => Assert.Equal(1, value),
value => Assert.Equal(2, value),
value => Assert.Equal(3, value));
},
b =>
chunk =>
{
Assert.Collection(
b,
ba => Assert.Equal(4, ba),
bb => Assert.Equal(5, bb),
bc => Assert.Equal(6, bc));
chunk,
value => Assert.Equal(4, value),
value => Assert.Equal(5, value),
value => Assert.Equal(6, value));
},
c =>
chunk =>
{
Assert.Collection(
c,
ca => Assert.Equal(7, ca),
cb => Assert.Equal(8, cb),
cc => Assert.Equal(9, cc));
chunk,
value => Assert.Equal(7, value),
value => Assert.Equal(8, value),
value => Assert.Equal(9, value));
});
}

Expand All @@ -84,18 +84,9 @@ public void GivenAnEnumerableNotAMultipleOfSizeWeHaveASmallerLastSlice()

Assert.Collection(
chunked,
a =>
{
Assert.Equal(chunkSize, a.Count);
},
b =>
{
Assert.Equal(chunkSize, b.Count);
},
c =>
{
Assert.Equal(numbers.Count % chunkSize, c.Count);
});
chunk => Assert.Equal(chunkSize, chunk.Count),
chunk => Assert.Equal(chunkSize, chunk.Count),
chunk => Assert.Equal(numbers.Count % chunkSize, chunk.Count));
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public void GivenASourceSequenceShorterThanTheSlidingWindowWidthReturnsAnEmptySe
}

[Fact]
public void SlidingWindowReturnsTheCorrectAmountOfWindowsAllOfEvenSize()
public void SlidingWindowReturnsTheCorrectAmountOfWindowsAllOfTheSameSize()
{
const int width = 5;
var source = Enumerable.Range(0, 10);
Expand Down Expand Up @@ -70,8 +70,8 @@ public void SlidingWindowReturnsASequenceOfConsecutiveWindows()

Assert.Collection(
source.SlidingWindow(width),
window1 => { Assert.Equal(Enumerable.Range(0, width), window1); },
window2 => { Assert.Equal(Enumerable.Range(1, width), window2); },
window3 => { Assert.Equal(Enumerable.Range(2, width), window3); });
window => Assert.Equal(Enumerable.Range(0, width), window),
window => Assert.Equal(Enumerable.Range(1, width), window),
window => Assert.Equal(Enumerable.Range(2, width), window));
}
}
Loading

0 comments on commit 0366510

Please sign in to comment.