Skip to content

Commit

Permalink
Implemented Chunk and SlidingWindow on string
Browse files Browse the repository at this point in the history
* Including tests
* improved naming on the old tests
* extracted common validation
* optimiziations
  • Loading branch information
FreeApophis committed Aug 30, 2023
1 parent 0f4dcaf commit d2ac623
Show file tree
Hide file tree
Showing 15 changed files with 265 additions and 96 deletions.
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");
}
5 changes: 4 additions & 1 deletion Funcky.Async/Funcky.Async.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisModeReliability>All</AnalysisModeReliability>
<EnablePackageValidation>true</EnablePackageValidation>
<NoWarn>$(NoWarn);RS0026</NoWarn><!-- RS0026: Do not add multiple overloads with optional parameters -->
<NoWarn>$(NoWarn);RS0026</NoWarn>
<!-- RS0026: Do not add multiple overloads with optional parameters -->
</PropertyGroup>
<PropertyGroup>
<RootNamespace>Funcky</RootNamespace>
Expand All @@ -35,6 +36,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); });
}
}
54 changes: 54 additions & 0 deletions Funcky.Test/Extensions/StringExtensions/ChunkOnStringTest.cs
Original file line number Diff line number Diff line change
@@ -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); });
}
}
Original file line number Diff line number Diff line change
@@ -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<ArgumentOutOfRangeException>(() => source.SlidingWindow(width));
}
}
Loading

0 comments on commit d2ac623

Please sign in to comment.