Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix bugs in Batch #587

Merged
merged 6 commits into from
Nov 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions Source/SuperLinq/Batch.Buffered.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ public static IEnumerable<TResult> Batch<TSource, TResult>(
ArgumentNullException.ThrowIfNull(array);
ArgumentNullException.ThrowIfNull(resultSelector);
ArgumentOutOfRangeException.ThrowIfLessThan(size, 1);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(size, array.Length);
ArgumentOutOfRangeException.ThrowIfGreaterThan(size, array.Length);

return BatchImpl(source, array, size, resultSelector);
}
Expand All @@ -166,12 +166,17 @@ private static IEnumerable<TResult> BatchImpl<TSource, TResult>(
int size,
Func<IReadOnlyList<TSource>, TResult> resultSelector)
{
if (source is ICollection<TSource> coll
&& coll.Count <= size)
if (source is ICollection<TSource> coll)
{
coll.CopyTo(array, 0);
yield return resultSelector(new ArraySegment<TSource>(array, 0, coll.Count));
yield break;
if (coll.Count == 0)
yield break;

if (coll.Count <= size)
{
coll.CopyTo(array, 0);
yield return resultSelector(new ArraySegment<TSource>(array, 0, coll.Count));
yield break;
}
}

var n = 0;
Expand Down
6 changes: 1 addition & 5 deletions Source/SuperLinq/Batch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,6 @@ static IEnumerable<IList<TSource>> Core(IEnumerable<TSource> source, int size)
yield break;
}
}
else if (source.TryGetCollectionCount() == 0)
{
yield break;
}

var n = 0;
foreach (var item in source)
Expand Down Expand Up @@ -101,7 +97,7 @@ public BatchIterator(IList<T> source, int size)
_size = size;
}

public override int Count => ((_source.Count - 1) / _size) + 1;
public override int Count => _source.Count == 0 ? 0 : ((_source.Count - 1) / _size) + 1;

protected override IEnumerable<IList<T>> GetEnumerable()
{
Expand Down
245 changes: 115 additions & 130 deletions Tests/SuperLinq.Test/BatchTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@

public class BatchTest
{
#region Regular
[Fact]
public void BatchIsLazy()
{
_ = new BreakingSequence<int>().Batch(1);
_ = new BreakingSequence<int>().Buffer(1);

_ = new BreakingSequence<int>()
.Batch(1, BreakingFunc.Of<IReadOnlyList<int>, int>());
_ = new BreakingSequence<int>()
.Batch(new int[2], BreakingFunc.Of<IReadOnlyList<int>, int>());
_ = new BreakingSequence<int>()
.Batch(new int[2], 1, BreakingFunc.Of<IReadOnlyList<int>, int>());
}

[Fact]
Expand All @@ -16,6 +22,21 @@ public void BatchValidatesSize()
_ = Assert.Throws<ArgumentOutOfRangeException>("size",
() => new BreakingSequence<int>()
.Batch(0));

_ = Assert.Throws<ArgumentOutOfRangeException>("size",
() => new BreakingSequence<int>()
.Batch(0, BreakingFunc.Of<IReadOnlyList<int>, int>()));

_ = Assert.Throws<ArgumentOutOfRangeException>("size",
() => new BreakingSequence<int>()
.Batch([], 0, BreakingFunc.Of<IReadOnlyList<int>, int>()));

_ = Assert.Throws<ArgumentOutOfRangeException>("size",
() => new BreakingSequence<int>()
.Batch(new int[5], 6, BreakingFunc.Of<IReadOnlyList<int>, int>()));

_ = new BreakingSequence<int>()
.Batch(new int[5], 5, BreakingFunc.Of<IReadOnlyList<int>, int>());
}

public static IEnumerable<object[]> GetFourElementSequences() =>
Expand Down Expand Up @@ -77,98 +98,147 @@ public void BatchModifiedDoesNotAffectPreviousBatch(IDisposableEnumerable<int> s
}
}

public enum BatchMethod
{
Traditional,
BufferSize,
BufferArray,
BufferSizeArray,
}

private static IEnumerable<object[]> GetBatchTestSequences(IEnumerable<int> source)
{
foreach (var seq in source.GetListSequences())
yield return new object[] { seq, BatchMethod.Traditional, };
yield return new object[] { source.AsTestingSequence(maxEnumerations: 2), BatchMethod.BufferSize, };
yield return new object[] { source.AsTestingSequence(maxEnumerations: 2), BatchMethod.BufferArray, };
yield return new object[] { source.AsTestingSequence(maxEnumerations: 2), BatchMethod.BufferSizeArray, };
}

private static IEnumerable<IList<T>> GetBatches<T>(
IEnumerable<T> seq,
BatchMethod method,
int size) =>
method switch
{
BatchMethod.Traditional => seq.Batch(size),
BatchMethod.BufferSize => seq.Batch(size, arr => arr.ToList()),
BatchMethod.BufferArray => seq.Batch(new T[size], arr => arr.ToList()),
BatchMethod.BufferSizeArray => seq.Batch(new T[size + 10], size, arr => arr.ToList()),
_ => throw new NotSupportedException(),
};

public static IEnumerable<object[]> GetEmptySequences() =>
Array.Empty<int>()
.GetAllSequences()
.Select(x => new object[] { x });
GetBatchTestSequences(Enumerable.Empty<int>());

[Theory]
[MemberData(nameof(GetEmptySequences))]
public void BatchWithEmptySource(IDisposableEnumerable<int> seq)
public void BatchWithEmptySource(IDisposableEnumerable<int> seq, BatchMethod bm)
{
using (seq)
Assert.Empty(seq.Batch(1));
}

[Fact]
// branch not able to run with `BreakingList<>`
public void BatchWithEmptyIListProvider()
{
Enumerable.Range(0, 0)
.Batch(1)
.AssertSequenceEqual();
{
var result = GetBatches(seq, bm, 5);
result.AssertSequenceEqual();
}
}

public static IEnumerable<object[]> GetSequences() =>
Enumerable.Range(1, 9)
.GetListSequences()
.Select(x => new object[] { x });
GetBatchTestSequences(Enumerable.Range(1, 9));

[Theory]
[MemberData(nameof(GetSequences))]
public void BatchEvenlyDivisibleSequence(IDisposableEnumerable<int> seq)
public void BatchEvenlyDivisibleSequence(IDisposableEnumerable<int> seq, BatchMethod bm)
{
using (seq)
{
var result = seq.Batch(3);

var result = GetBatches(seq, bm, 3);
result.AssertSequenceEqual(
Seq(1, 2, 3),
Seq(4, 5, 6),
Seq(7, 8, 9));
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]);
}
}

[Theory]
[MemberData(nameof(GetSequences))]
public void BatchUnevenlyDivisibleSequence(IDisposableEnumerable<int> seq)
public void BatchUnevenlyDivisibleSequence(IDisposableEnumerable<int> seq, BatchMethod bm)
{
using (seq)
{
var result = seq.Batch(4);

var result = GetBatches(seq, bm, 4);
result.AssertSequenceEqual(
Seq(1, 2, 3, 4),
Seq(5, 6, 7, 8),
Seq(9));
[1, 2, 3, 4],
[5, 6, 7, 8],
[9]);
}
}

[Theory]
[MemberData(nameof(GetSequences))]
public void BatchSmallSequence(IDisposableEnumerable<int> seq)
public void BatchSmallSequence(IDisposableEnumerable<int> seq, BatchMethod bm)
{
using (seq)
{
var result = seq.Batch(10);

var result = GetBatches(seq, bm, 10);
result.AssertSequenceEqual(
Seq(1, 2, 3, 4, 5, 6, 7, 8, 9));
[1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
}

[Fact]
public void BatchWithCollectionSmallerThanBatchSize()
public static IEnumerable<object[]> GetBreakingCollections(IEnumerable<int> source)
{
using var seq = new BreakingCollection<int>(Enumerable.Range(1, 9));
seq.Batch(10).Consume();
yield return new object[] { source.AsBreakingCollection(), BatchMethod.Traditional, };
yield return new object[] { source.AsBreakingCollection(), BatchMethod.BufferSize, };
yield return new object[] { source.AsBreakingCollection(), BatchMethod.BufferArray, };
yield return new object[] { source.AsBreakingCollection(), BatchMethod.BufferSizeArray, };
}

[Fact]
public void BatchCollectionSizeNotEvaluatedEarly()
[Theory]
[MemberData(nameof(GetBreakingCollections), new int[] { })]
public void BatchWithEmptyCollection(IDisposableEnumerable<int> seq, BatchMethod bm)
{
var list = new List<int>(Enumerable.Range(1, 3));
var result = list.Batch(3);
using (seq)
{
var result = GetBatches(seq, bm, 10);
result.AssertSequenceEqual();
}
}

[Theory]
[MemberData(nameof(GetBreakingCollections), new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 })]
public void BatchWithCollectionSmallerThanBatchSize(IDisposableEnumerable<int> seq, BatchMethod bm)
{
using (seq)
{
var result = GetBatches(seq, bm, 10);
result.AssertSequenceEqual(
[1, 2, 3, 4, 5, 6, 7, 8, 9]);
}
}

[Theory]
[InlineData(BatchMethod.Traditional)]
[InlineData(BatchMethod.BufferSize)]
[InlineData(BatchMethod.BufferArray)]
[InlineData(BatchMethod.BufferSizeArray)]
public void BatchCollectionSizeNotEvaluatedEarly(BatchMethod bm)
{
var list = new List<int>() { 1, 2, 3, };
var result = GetBatches(list, bm, 3);
list.Add(4);
result.AssertCount(2).Consume();
}

[Fact]
public void BatchUsesCollectionCountAtIterationTime()
[Theory]
[InlineData(BatchMethod.Traditional)]
[InlineData(BatchMethod.BufferSize)]
[InlineData(BatchMethod.BufferArray)]
[InlineData(BatchMethod.BufferSizeArray)]
public void BatchUsesCollectionCountAtIterationTime(BatchMethod bm)
{
var list = new List<int>(Enumerable.Range(1, 3));
using var ts = new BreakingCollection<int>(list);
var result = ts.Batch(3);
var result = GetBatches(ts, bm, 3);

// should use `CopyTo`
result.AssertCount(1).Consume();
Expand Down Expand Up @@ -208,89 +278,4 @@ public void BatchListUnevenlyDivisibleBehavior()
Assert.Equal(Enumerable.Range(9_980, 20), result.ElementAt(^2));
Assert.Equal(Enumerable.Range(10_000, 2), result.ElementAt(^1));
}
#endregion

#region Buffered
[Fact]
public void BatchBufferedIsLazy()
{
_ = new BreakingSequence<int>()
.Batch(1, BreakingFunc.Of<IReadOnlyList<int>, int>());
_ = new BreakingSequence<int>()
.Batch(new int[2], BreakingFunc.Of<IReadOnlyList<int>, int>());
_ = new BreakingSequence<int>()
.Batch(new int[2], 1, BreakingFunc.Of<IReadOnlyList<int>, int>());
}

[Fact]
public void BatchBufferedValidatesSize()
{
_ = Assert.Throws<ArgumentOutOfRangeException>("size",
() => new BreakingSequence<int>()
.Batch(0, BreakingFunc.Of<IReadOnlyList<int>, int>()));
_ = Assert.Throws<ArgumentOutOfRangeException>("size",
() => new BreakingSequence<int>()
.Batch(new int[2], 0, BreakingFunc.Of<IReadOnlyList<int>, int>()));
_ = Assert.Throws<ArgumentOutOfRangeException>("size",
() => new BreakingSequence<int>()
.Batch(new int[2], 3, BreakingFunc.Of<IReadOnlyList<int>, int>()));
}

[Fact]
public void BatchBufferedWithEmptySource()
{
using var xs = TestingSequence.Of<int>();
Assert.Empty(xs.Batch(1, BreakingFunc.Of<IReadOnlyList<int>, int>()));
}

[Fact]
public void BatchBufferedEvenlyDivisibleSequence()
{
using var seq = Enumerable.Range(1, 9).AsTestingSequence();

var result = seq.Batch(3, l => string.Join(",", l));
using var reader = result.Read();
Assert.Equal("1,2,3", reader.Read());
Assert.Equal("4,5,6", reader.Read());
Assert.Equal("7,8,9", reader.Read());
reader.ReadEnd();
}

[Fact]
public void BatchBufferedUnevenlyDivisibleSequence()
{
using var seq = Enumerable.Range(1, 9).AsTestingSequence();

var result = seq.Batch(4, l => string.Join(",", l));
using var reader = result.Read();
Assert.Equal("1,2,3,4", reader.Read());
Assert.Equal("5,6,7,8", reader.Read());
Assert.Equal("9", reader.Read());
reader.ReadEnd();
}

[Fact]
public void BatchBufferedWithCollectionSmallerThanBatchSize()
{
using var seq = new BreakingCollection<int>(Enumerable.Range(1, 9));
seq.Batch(10, i => i.Sum()).Consume();
}

[Fact]
public void BatchBufferedUsesCollectionCountAtIterationTime()
{
var list = new List<int>(Enumerable.Range(1, 3));
using var ts = new BreakingCollection<int>(list);
var result = ts.Batch(3, w => w[0]);

// should use `CopyTo`
result.AssertCount(1).Consume();

list.Add(4);

// should fail trying to enumerate
_ = Assert.Throws<TestException>(
() => result.AssertCount(2).Consume());
}
#endregion
}