From d2ac6233b2e03adddf556d7013b4b615311f396c Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 30 Aug 2023 09:25:15 +0200 Subject: [PATCH 1/5] Implemented Chunk and SlidingWindow on string * Including tests * improved naming on the old tests * extracted common validation * optimiziations --- .../AsyncEnumerableExtensions/ChunkTest.cs | 45 ++++++--------- .../SlidingWindowTest.cs | 8 +-- .../AsyncEnumerableExtensions/Chunk.cs | 14 ++--- .../SlidingWindow.cs | 9 +-- Funcky.Async/Funcky.Async.csproj | 5 +- .../EnumerableExtensions/ChunkTest.cs | 51 +++++++---------- .../EnumerableExtensions/SlidingWindowTest.cs | 8 +-- .../StringExtensions/ChunkOnStringTest.cs | 54 ++++++++++++++++++ .../SlidingWindowOnStringTest.cs | 57 +++++++++++++++++++ .../Extensions/EnumerableExtensions/Chunk.cs | 10 +--- .../EnumerableExtensions/SlidingWindow.cs | 9 +-- Funcky/Extensions/StringExtensions/Chunk.cs | 40 +++++++++++++ .../StringExtensions/SlidingWindow.cs | 27 +++++++++ .../Internal/Validators/ChunkSizeValidator.cs | 12 ++++ .../Validators/WindowWidthValidator.cs | 12 ++++ 15 files changed, 265 insertions(+), 96 deletions(-) create mode 100644 Funcky.Test/Extensions/StringExtensions/ChunkOnStringTest.cs create mode 100644 Funcky.Test/Extensions/StringExtensions/SlidingWindowOnStringTest.cs create mode 100644 Funcky/Extensions/StringExtensions/Chunk.cs create mode 100644 Funcky/Extensions/StringExtensions/SlidingWindow.cs create mode 100644 Funcky/Internal/Validators/ChunkSizeValidator.cs create mode 100644 Funcky/Internal/Validators/WindowWidthValidator.cs diff --git a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/ChunkTest.cs b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/ChunkTest.cs index dedef74a..72f85444 100644 --- a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/ChunkTest.cs +++ b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/ChunkTest.cs @@ -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)); }); } @@ -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] diff --git a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/SlidingWindowTest.cs b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/SlidingWindowTest.cs index 4feee190..a441560e 100644 --- a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/SlidingWindowTest.cs +++ b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/SlidingWindowTest.cs @@ -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); @@ -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] diff --git a/Funcky.Async/Extensions/AsyncEnumerableExtensions/Chunk.cs b/Funcky.Async/Extensions/AsyncEnumerableExtensions/Chunk.cs index c001000d..fcd2ceda 100644 --- a/Funcky.Async/Extensions/AsyncEnumerableExtensions/Chunk.cs +++ b/Funcky.Async/Extensions/AsyncEnumerableExtensions/Chunk.cs @@ -1,4 +1,5 @@ using System.Runtime.CompilerServices; +using Funcky.Internal.Validators; namespace Funcky.Extensions; @@ -13,7 +14,7 @@ public static partial class AsyncEnumerableExtensions /// A sequence of equally sized sequences containing elements of the source collection in the same order. [Pure] public static IAsyncEnumerable> Chunk(this IAsyncEnumerable source, int size) - => ChunkEnumerable(source, ValidateChunkSize(size)); + => ChunkEnumerable(source, ChunkSizeValidator.Validate(size)); /// /// Chunks the source sequence into equally sized chunks. The last chunk can be smaller. @@ -26,7 +27,7 @@ public static IAsyncEnumerable> Chunk(this IAsyn /// A sequence of results based on equally sized chunks. [Pure] public static IAsyncEnumerable Chunk(this IAsyncEnumerable source, int size, Func, TResult> resultSelector) - => ChunkEnumerable(source, ValidateChunkSize(size)) + => ChunkEnumerable(source, ChunkSizeValidator.Validate(size)) .Select(resultSelector); /// @@ -40,7 +41,7 @@ public static IAsyncEnumerable Chunk(this IAsyncEnume /// A sequence of results based on equally sized chunks. [Pure] public static IAsyncEnumerable ChunkAwait(this IAsyncEnumerable source, int size, Func, ValueTask> resultSelector) - => ChunkEnumerable(source, ValidateChunkSize(size)) + => ChunkEnumerable(source, ChunkSizeValidator.Validate(size)) .SelectAwait(resultSelector); /// @@ -54,14 +55,9 @@ public static IAsyncEnumerable ChunkAwait(this IAsync /// A sequence of results based on equally sized chunks. [Pure] public static IAsyncEnumerable ChunkAwaitWithCancellation(this IAsyncEnumerable source, int size, Func, CancellationToken, ValueTask> 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> ChunkEnumerable(IAsyncEnumerable source, int size, [EnumeratorCancellation] CancellationToken cancellationToken = default) { var asyncEnumerator = source.GetAsyncEnumerator(cancellationToken); diff --git a/Funcky.Async/Extensions/AsyncEnumerableExtensions/SlidingWindow.cs b/Funcky.Async/Extensions/AsyncEnumerableExtensions/SlidingWindow.cs index 1f62d305..2a4e7989 100644 --- a/Funcky.Async/Extensions/AsyncEnumerableExtensions/SlidingWindow.cs +++ b/Funcky.Async/Extensions/AsyncEnumerableExtensions/SlidingWindow.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using Funcky.Internal; +using Funcky.Internal.Validators; namespace Funcky.Extensions; @@ -19,7 +20,7 @@ public static partial class AsyncEnumerableExtensions /// Returns a sequence of equally sized window sequences. [Pure] public static IAsyncEnumerable> SlidingWindow(this IAsyncEnumerable source, int width) - => SlidingWindowEnumerable(source, ValidateWindowWidth(width)); + => SlidingWindowEnumerable(source, WindowWidthValidator.Validate(width)); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static async IAsyncEnumerable> SlidingWindowEnumerable( @@ -36,10 +37,4 @@ private static async IAsyncEnumerable> 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"); } diff --git a/Funcky.Async/Funcky.Async.csproj b/Funcky.Async/Funcky.Async.csproj index f508cd7a..e4bbbe5a 100644 --- a/Funcky.Async/Funcky.Async.csproj +++ b/Funcky.Async/Funcky.Async.csproj @@ -14,7 +14,8 @@ true All true - $(NoWarn);RS0026 + $(NoWarn);RS0026 + Funcky @@ -35,6 +36,8 @@ + + diff --git a/Funcky.Test/Extensions/EnumerableExtensions/ChunkTest.cs b/Funcky.Test/Extensions/EnumerableExtensions/ChunkTest.cs index 7f3ffd2b..90f273cc 100644 --- a/Funcky.Test/Extensions/EnumerableExtensions/ChunkTest.cs +++ b/Funcky.Test/Extensions/EnumerableExtensions/ChunkTest.cs @@ -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)); }); } @@ -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)); }); } @@ -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] diff --git a/Funcky.Test/Extensions/EnumerableExtensions/SlidingWindowTest.cs b/Funcky.Test/Extensions/EnumerableExtensions/SlidingWindowTest.cs index d5aedda0..fe01b9c5 100644 --- a/Funcky.Test/Extensions/EnumerableExtensions/SlidingWindowTest.cs +++ b/Funcky.Test/Extensions/EnumerableExtensions/SlidingWindowTest.cs @@ -40,7 +40,7 @@ public void GivenASourceSequenceShorterThanTheSlidingWindowWidthReturnsAnEmptySe } [Fact] - public void SlidingWindowReturnsTheCorrectAmountOfWindowsAllOfEvenSize() + public void SlidingWindowReturnsTheCorrectAmountOfWindowsAllOfTheSameSize() { const int width = 5; var source = Enumerable.Range(0, 10); @@ -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); }); } } diff --git a/Funcky.Test/Extensions/StringExtensions/ChunkOnStringTest.cs b/Funcky.Test/Extensions/StringExtensions/ChunkOnStringTest.cs new file mode 100644 index 00000000..434ca249 --- /dev/null +++ b/Funcky.Test/Extensions/StringExtensions/ChunkOnStringTest.cs @@ -0,0 +1,54 @@ +using FsCheck; +using FsCheck.Xunit; + +namespace Funcky.Test.Extensions.StringExtensions; + +public class ChunkOnStringTest +{ + [Property] + public Property ChunkingAnEmptyStringWillAlwaysYieldAndEmptySequence(PositiveInt width) + => string.Empty + .Chunk(width.Get) + .None() + .ToProperty(); + + [Fact] + public void GivenAnSingleElementListWeGetEnumerableWithOneElement() + { + var value = "a"; + + var chunked = value.Chunk(3); + + Assert.Collection( + chunked, + chunk => Assert.Equal("a", chunk)); + } + + [Fact] + public void GivenAnStringWhichIsNotAMultipleOfSizeWeHaveASmallerLastString() + { + var value = "abcdefghij"; + + const int chunkSize = 4; + var chunked = value.Chunk(chunkSize); + + Assert.Collection( + chunked, + chunk => Assert.Equal(chunkSize, chunk.Length), + chunk => Assert.Equal(chunkSize, chunk.Length), + chunk => Assert.Equal(value.Length % chunkSize, chunk.Length)); + } + + [Fact] + public void AChunkOfAStringReturnsAListOfConsecutivePartialStrings() + { + const int width = 3; + var source = "epsilon"; + + Assert.Collection( + source.Chunk(width), + chunk => { Assert.Equal("eps", chunk); }, + chunk => { Assert.Equal("ilo", chunk); }, + chunk => { Assert.Equal("n", chunk); }); + } +} diff --git a/Funcky.Test/Extensions/StringExtensions/SlidingWindowOnStringTest.cs b/Funcky.Test/Extensions/StringExtensions/SlidingWindowOnStringTest.cs new file mode 100644 index 00000000..6990fe30 --- /dev/null +++ b/Funcky.Test/Extensions/StringExtensions/SlidingWindowOnStringTest.cs @@ -0,0 +1,57 @@ +using FsCheck; +using FsCheck.Xunit; + +namespace Funcky.Test.Extensions.StringExtensions; + +public class SlidingWindowOnStringTest +{ + [Property] + public Property ASlidingWindowFromAnEmptyStringWillAlwaysYieldAnEmptySequence(PositiveInt width) + => string.Empty + .SlidingWindow(width.Get) + .None() + .ToProperty(); + + [Fact] + public void SlidingWindowReturnsAListOfOverlappingPartialStrings() + { + const int width = 4; + var source = "epsilon"; + + Assert.Collection( + source.SlidingWindow(width), + window => { Assert.Equal("epsi", window); }, + window => { Assert.Equal("psil", window); }, + window => { Assert.Equal("silo", window); }, + window => { Assert.Equal("ilon", window); }); + } + + [Fact] + public void GivenASourceStringEqualInLengthToTheSlidingWindowWidthReturnsASequenceWithOneElement() + { + var source = "gamma"; + + Assert.Single(source.SlidingWindow(5)); + Assert.Equal(source, source.SlidingWindow(5).Single()); + } + + [Fact] + public void GivenASourceSequenceShorterThanTheSlidingWindowWidthReturnsAnEmptySequence() + { + var source = "gamma"; + + Assert.Empty(source.SlidingWindow(10)); + } + + [Theory] + [InlineData(int.MinValue)] + [InlineData(-42)] + [InlineData(-1)] + [InlineData(0)] + public void SlidingWindowThrowsOnNonPositiveWidth(int width) + { + var source = "Just a simple test!"; + + Assert.Throws(() => source.SlidingWindow(width)); + } +} diff --git a/Funcky/Extensions/EnumerableExtensions/Chunk.cs b/Funcky/Extensions/EnumerableExtensions/Chunk.cs index fcbd0c5e..85eac260 100644 --- a/Funcky/Extensions/EnumerableExtensions/Chunk.cs +++ b/Funcky/Extensions/EnumerableExtensions/Chunk.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using Funcky.Internal.Validators; namespace Funcky.Extensions; @@ -18,7 +19,7 @@ public static IEnumerable> Chunk(IEnumerable> Chunk(this IEnumerable source, int size) #endif - => ChunkEnumerable(source, ValidateChunkSize(size)); + => ChunkEnumerable(source, ChunkSizeValidator.Validate(size)); /// /// Chunks the source sequence into equally sized chunks. The last chunk can be smaller. @@ -31,14 +32,9 @@ public static IEnumerable> Chunk(this IEnumerabl /// A sequence of results based on equally sized chunks. [Pure] public static IEnumerable Chunk(this IEnumerable source, int size, Func, TResult> resultSelector) - => ChunkEnumerable(source, ValidateChunkSize(size)) + => ChunkEnumerable(source, ChunkSizeValidator.Validate(size)) .Select(resultSelector); - private static int ValidateChunkSize(int size) - => size > 0 - ? size - : throw new ArgumentOutOfRangeException(nameof(size), size, "Size must be bigger than 0"); - private static IEnumerable> ChunkEnumerable(IEnumerable source, int size) { using var sourceEnumerator = source.GetEnumerator(); diff --git a/Funcky/Extensions/EnumerableExtensions/SlidingWindow.cs b/Funcky/Extensions/EnumerableExtensions/SlidingWindow.cs index 2cc44c45..8140a966 100644 --- a/Funcky/Extensions/EnumerableExtensions/SlidingWindow.cs +++ b/Funcky/Extensions/EnumerableExtensions/SlidingWindow.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using Funcky.Internal; +using Funcky.Internal.Validators; namespace Funcky.Extensions; @@ -19,7 +20,7 @@ public static partial class EnumerableExtensions /// Returns a sequence of equally sized window sequences. [Pure] public static IEnumerable> SlidingWindow(this IEnumerable source, int width) - => SlidingWindowEnumerable(source, ValidateWindowWidth(width)); + => SlidingWindowEnumerable(source, WindowWidthValidator.Validate(width)); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static IEnumerable> SlidingWindowEnumerable(IEnumerable source, int width) @@ -33,10 +34,4 @@ private static IEnumerable> SlidingWindowEnumerable width > 0 - ? width - : throw new ArgumentOutOfRangeException(nameof(width), width, "The width of the window must be larger than 0"); } diff --git a/Funcky/Extensions/StringExtensions/Chunk.cs b/Funcky/Extensions/StringExtensions/Chunk.cs new file mode 100644 index 00000000..ccc77e30 --- /dev/null +++ b/Funcky/Extensions/StringExtensions/Chunk.cs @@ -0,0 +1,40 @@ +using System.Runtime.CompilerServices; +using Funcky.Internal.Validators; + +namespace Funcky.Extensions; + +public static partial class StringExtensions +{ + private const int LastElement = 1; + + /// + /// Chunks the source string into equally sized chunks. The last chunk can be smaller. + /// + /// The source string. + /// The desired size of the chunks in characters. + /// A sequence of equally sized substrings containing consecutive substrings of the source string in the same order. + public static IEnumerable Chunk(this string source, int size) + => ChunkString(source, ChunkSizeValidator.Validate(size)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static IEnumerable ChunkString(string source, int size) + { + var count = DivisionWithCeiling(source.Length, size); + + // optimization: we do not emit the last chunk, because it might be smaller than size. + var index = 0; + for (; index < count - LastElement; ++index) + { + yield return source.Substring(index * size, size); + } + + // If there is anything left to emit, we will emit the last chunk. + if (source.Length > 0) + { + yield return source.Substring(index * size); + } + } + + private static int DivisionWithCeiling(int dividend, int divisior) + => (dividend + divisior - 1) / divisior; +} diff --git a/Funcky/Extensions/StringExtensions/SlidingWindow.cs b/Funcky/Extensions/StringExtensions/SlidingWindow.cs new file mode 100644 index 00000000..e1e5646f --- /dev/null +++ b/Funcky/Extensions/StringExtensions/SlidingWindow.cs @@ -0,0 +1,27 @@ +using System.Runtime.CompilerServices; +using Funcky.Internal.Validators; + +namespace Funcky.Extensions; + +public static partial class StringExtensions +{ + /// + /// SlidingWindow returns a sequence of sliding window strings of the given width. + /// The nth string will start with the nth character of the source sequence. + /// + /// The source string. + /// The width of the sliding window in characters. + /// Returns a sequence of equally sized substrings. + public static IEnumerable SlidingWindow(this string source, int width) + => StringSlidingWindow(source, WindowWidthValidator.Validate(width)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static IEnumerable StringSlidingWindow(string source, int width) + { + var numberOfWindows = source.Length - width + 1; + for (var index = 0; index < numberOfWindows; ++index) + { + yield return source.Substring(index, width); + } + } +} diff --git a/Funcky/Internal/Validators/ChunkSizeValidator.cs b/Funcky/Internal/Validators/ChunkSizeValidator.cs new file mode 100644 index 00000000..c3ce35da --- /dev/null +++ b/Funcky/Internal/Validators/ChunkSizeValidator.cs @@ -0,0 +1,12 @@ +using System.Runtime.CompilerServices; + +namespace Funcky.Internal.Validators; + +internal static class ChunkSizeValidator +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Validate(int size) + => size > 0 + ? size + : throw new ArgumentOutOfRangeException(nameof(size), size, "Size must be bigger than 0"); +} diff --git a/Funcky/Internal/Validators/WindowWidthValidator.cs b/Funcky/Internal/Validators/WindowWidthValidator.cs new file mode 100644 index 00000000..94885e4a --- /dev/null +++ b/Funcky/Internal/Validators/WindowWidthValidator.cs @@ -0,0 +1,12 @@ +using System.Runtime.CompilerServices; + +namespace Funcky.Internal.Validators; + +internal static class WindowWidthValidator +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Validate(int width) + => width > 0 + ? width + : throw new ArgumentOutOfRangeException(nameof(width), width, "The width of the window must be larger than 0"); +} From 1acff7e06bf5e9acd36054aedb670e89e20aa6a5 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 30 Aug 2023 09:29:57 +0200 Subject: [PATCH 2/5] Add new API --- Funcky/PublicAPI.Unshipped.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Funcky/PublicAPI.Unshipped.txt b/Funcky/PublicAPI.Unshipped.txt index b6f13844..99809c74 100644 --- a/Funcky/PublicAPI.Unshipped.txt +++ b/Funcky/PublicAPI.Unshipped.txt @@ -10,3 +10,5 @@ Funcky.Monads.Result.InspectError(System.Action Funcky.Monads.Result.OrElse(Funcky.Monads.Result fallback) -> Funcky.Monads.Result Funcky.Monads.Result.OrElse(System.Func>! fallback) -> Funcky.Monads.Result static Funcky.Extensions.EnumeratorExtensions.MoveNextOrNone(this System.Collections.Generic.IEnumerator! enumerator) -> Funcky.Monads.Option +static Funcky.Extensions.StringExtensions.Chunk(this string! source, int size) -> System.Collections.Generic.IEnumerable! +static Funcky.Extensions.StringExtensions.SlidingWindow(this string! source, int width) -> System.Collections.Generic.IEnumerable! From c41638e8ba91351ac137190f4f0669b9ec3cf757 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 30 Aug 2023 09:35:44 +0200 Subject: [PATCH 3/5] * better spacing * forgotten inlining attribute --- Funcky/Extensions/StringExtensions/Chunk.cs | 1 + Funcky/Extensions/StringExtensions/SlidingWindow.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/Funcky/Extensions/StringExtensions/Chunk.cs b/Funcky/Extensions/StringExtensions/Chunk.cs index ccc77e30..f3404f61 100644 --- a/Funcky/Extensions/StringExtensions/Chunk.cs +++ b/Funcky/Extensions/StringExtensions/Chunk.cs @@ -35,6 +35,7 @@ private static IEnumerable ChunkString(string source, int size) } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int DivisionWithCeiling(int dividend, int divisior) => (dividend + divisior - 1) / divisior; } diff --git a/Funcky/Extensions/StringExtensions/SlidingWindow.cs b/Funcky/Extensions/StringExtensions/SlidingWindow.cs index e1e5646f..0c0730c6 100644 --- a/Funcky/Extensions/StringExtensions/SlidingWindow.cs +++ b/Funcky/Extensions/StringExtensions/SlidingWindow.cs @@ -19,6 +19,7 @@ public static IEnumerable SlidingWindow(this string source, int width) private static IEnumerable StringSlidingWindow(string source, int width) { var numberOfWindows = source.Length - width + 1; + for (var index = 0; index < numberOfWindows; ++index) { yield return source.Substring(index, width); From e4edc515185ea440d70a39a3c3701128ca9c1ce3 Mon Sep 17 00:00:00 2001 From: Thomas Bruderer Date: Wed, 30 Aug 2023 09:46:44 +0200 Subject: [PATCH 4/5] address review --- .../AsyncEnumerableExtensions/SlidingWindowTest.cs | 6 +++--- Funcky.Async/Funcky.Async.csproj | 3 +-- .../Extensions/EnumerableExtensions/SlidingWindowTest.cs | 6 +++--- .../Extensions/StringExtensions/ChunkOnStringTest.cs | 8 ++++---- .../StringExtensions/SlidingWindowOnStringTest.cs | 8 ++++---- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/SlidingWindowTest.cs b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/SlidingWindowTest.cs index a441560e..fe79ebb3 100644 --- a/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/SlidingWindowTest.cs +++ b/Funcky.Async.Test/Extensions/AsyncEnumerableExtensions/SlidingWindowTest.cs @@ -74,9 +74,9 @@ public async Task SlidingWindowReturnsASequenceOfConsecutiveWindowsAsync() await AsyncAssert.Collection( source.SlidingWindow(width), - window => { Assert.Equal(Enumerable.Range(0, width), window); }, - window => { Assert.Equal(Enumerable.Range(1, width), window); }, - window => { Assert.Equal(Enumerable.Range(2, width), window); }); + 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] diff --git a/Funcky.Async/Funcky.Async.csproj b/Funcky.Async/Funcky.Async.csproj index e4bbbe5a..da8da973 100644 --- a/Funcky.Async/Funcky.Async.csproj +++ b/Funcky.Async/Funcky.Async.csproj @@ -14,8 +14,7 @@ true All true - $(NoWarn);RS0026 - + $(NoWarn);RS0026 Funcky diff --git a/Funcky.Test/Extensions/EnumerableExtensions/SlidingWindowTest.cs b/Funcky.Test/Extensions/EnumerableExtensions/SlidingWindowTest.cs index fe01b9c5..6d528635 100644 --- a/Funcky.Test/Extensions/EnumerableExtensions/SlidingWindowTest.cs +++ b/Funcky.Test/Extensions/EnumerableExtensions/SlidingWindowTest.cs @@ -70,8 +70,8 @@ public void SlidingWindowReturnsASequenceOfConsecutiveWindows() Assert.Collection( source.SlidingWindow(width), - window => { Assert.Equal(Enumerable.Range(0, width), window); }, - window => { Assert.Equal(Enumerable.Range(1, width), window); }, - window => { Assert.Equal(Enumerable.Range(2, width), window); }); + window => Assert.Equal(Enumerable.Range(0, width), window), + window => Assert.Equal(Enumerable.Range(1, width), window), + window => Assert.Equal(Enumerable.Range(2, width), window)); } } diff --git a/Funcky.Test/Extensions/StringExtensions/ChunkOnStringTest.cs b/Funcky.Test/Extensions/StringExtensions/ChunkOnStringTest.cs index 434ca249..e17531ac 100644 --- a/Funcky.Test/Extensions/StringExtensions/ChunkOnStringTest.cs +++ b/Funcky.Test/Extensions/StringExtensions/ChunkOnStringTest.cs @@ -7,10 +7,10 @@ public class ChunkOnStringTest { [Property] public Property ChunkingAnEmptyStringWillAlwaysYieldAndEmptySequence(PositiveInt width) - => string.Empty - .Chunk(width.Get) - .None() - .ToProperty(); + => string.Empty + .Chunk(width.Get) + .None() + .ToProperty(); [Fact] public void GivenAnSingleElementListWeGetEnumerableWithOneElement() diff --git a/Funcky.Test/Extensions/StringExtensions/SlidingWindowOnStringTest.cs b/Funcky.Test/Extensions/StringExtensions/SlidingWindowOnStringTest.cs index 6990fe30..36bbbd70 100644 --- a/Funcky.Test/Extensions/StringExtensions/SlidingWindowOnStringTest.cs +++ b/Funcky.Test/Extensions/StringExtensions/SlidingWindowOnStringTest.cs @@ -20,10 +20,10 @@ public void SlidingWindowReturnsAListOfOverlappingPartialStrings() Assert.Collection( source.SlidingWindow(width), - window => { Assert.Equal("epsi", window); }, - window => { Assert.Equal("psil", window); }, - window => { Assert.Equal("silo", window); }, - window => { Assert.Equal("ilon", window); }); + window => Assert.Equal("epsi", window), + window => Assert.Equal("psil", window), + window => Assert.Equal("silo", window), + window => Assert.Equal("ilon", window)); } [Fact] From f21f35cf420cde39692a877198dccf9671ae4343 Mon Sep 17 00:00:00 2001 From: Tau <4602612+bash@users.noreply.github.com> Date: Mon, 11 Sep 2023 11:24:21 +0200 Subject: [PATCH 5/5] Update out-of-date Cartesian product docs --- .../cartesian-product.md | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/Documentation/src/enumerable-extensions/cartesian-product.md b/Documentation/src/enumerable-extensions/cartesian-product.md index 96b7528f..4d909744 100644 --- a/Documentation/src/enumerable-extensions/cartesian-product.md +++ b/Documentation/src/enumerable-extensions/cartesian-product.md @@ -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}"); +```