Skip to content

Commit

Permalink
Add .BindByIndex() (#43)
Browse files Browse the repository at this point in the history
* Add sync .BindByIndex()

* Add async .BindByIndex()

* Add documentation
  • Loading branch information
viceroypenguin authored Aug 11, 2022
1 parent 1b8509b commit b942b7f
Show file tree
Hide file tree
Showing 7 changed files with 471 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ The documentation for the SuperLinq.Async methods can be found [here](Source/Sup
| AtMost | ✔️ | ✔️ |
| Backsert | ✔️[^2] |[^2] |
| Batch | ❌<br/>(Removed[^3]) |[^3] |
| BindByIndex | ✔️ | ✔️ |
| Cartesian | ✔️ ||
| Choose | ✔️ | ✔️ |
| CountBetween | ✔️ | ✔️ |
Expand Down
126 changes: 126 additions & 0 deletions Source/SuperLinq.Async/BindByIndex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
namespace SuperLinq.Async;

public static partial class AsyncSuperEnumerable
{
/// <summary>
/// Selects elements by index from a sequence.
/// </summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="source"/>.</typeparam>
/// <param name="source">The source sequence.</param>
/// <param name="indices">The list of indices of elements in the <paramref name="source"/> sequence to select.</param>
/// <returns>
/// An <see cref="IAsyncEnumerable{T}"/> whose elements are the result of selecting elements according to the <paramref name="indices"/> sequence.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="indices"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException">An index in <paramref name="indices"/> is out of range for the input sequence <paramref name="source"/>.</exception>
public static IAsyncEnumerable<TSource> BindByIndex<TSource>(
this IAsyncEnumerable<TSource> source,
IAsyncEnumerable<int> indices)
{
#pragma warning disable MA0015
return BindByIndex(source, indices, static (e, i) => e, static i => throw new ArgumentOutOfRangeException(nameof(indices), "Index is greater than the length of the first sequence."));
#pragma warning restore MA0015
}

/// <summary>
/// Selects elements by index from a sequence and transforms them using the provided functions.
/// </summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="source"/>.</typeparam>
/// <typeparam name="TResult">The type of the elements of the resulting sequence.</typeparam>
/// <param name="source">The source sequence.</param>
/// <param name="indices">The list of indices of elements in the <paramref name="source"/> sequence to select.</param>
/// <param name="resultSelector">A transform function to apply to each source element; the second parameter of the function represents the index of the output sequence.</param>
/// <param name="missingSelector">A transform function to apply to missing source elements; the parameter represents the index of the output sequence.</param>
/// <returns>
/// An <see cref="IAsyncEnumerable{T}"/> whose elements are the result of selecting elements according to the <paramref name="indices"/> sequence
/// and invoking the transform function.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="indices"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="resultSelector"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="missingSelector"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This method uses deferred execution and streams its results.
/// </para>
/// </remarks>
public static IAsyncEnumerable<TResult> BindByIndex<TSource, TResult>(
this IAsyncEnumerable<TSource> source,
IAsyncEnumerable<int> indices,
Func<TSource, int, TResult> resultSelector,
Func<int, TResult> missingSelector)
{
Guard.IsNotNull(source);
Guard.IsNotNull(indices);
Guard.IsNotNull(resultSelector);
Guard.IsNotNull(missingSelector);

return _(source, indices, resultSelector, missingSelector);

static async IAsyncEnumerable<TResult> _(
IAsyncEnumerable<TSource> source, IAsyncEnumerable<int> indices, Func<TSource, int, TResult> resultSelector, Func<int, TResult> missingSelector,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// keeps track of the order of indices to know what order items should be output in
var lookup = await indices.Index().ToDictionaryAsync(x => { Guard.IsGreaterThanOrEqualTo(x.item, 0, nameof(indices)); return x.item; }, x => x.index, cancellationToken).ConfigureAwait(false);
// keep track of items out of output order
var lookback = new Dictionary<int, TSource>();

// which input index are we on?
var index = 0;
// which output index are we on?
var outputIndex = 0;

// for each item in input
await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false))
{
// does the current input index have an output?
if (lookup.TryGetValue(index, out var oi))
{
// is the current item's output order the next one?
if (oi == outputIndex)
{
// return the item and increment output order
yield return resultSelector(item, outputIndex);
outputIndex++;

// while we're here, catch up on any lookbacks
while (lookback.TryGetValue(outputIndex, out var e))
{
yield return resultSelector(e, outputIndex);
lookback.Remove(outputIndex);
outputIndex++;
}
}
// otherwise, store in lookback for later
else
{
lookback[oi] = item;
}
}

index++;
}

// catch up any remaining items
while (outputIndex < lookup.Count)
{
// can we find the current output index in lookback?
if (lookback.TryGetValue(outputIndex, out var e))
{
// return it
yield return resultSelector(e, outputIndex);
lookback.Remove(outputIndex);
}
else
{
// otherwise, return a missing item
yield return missingSelector(outputIndex);
}

outputIndex++;
}
}
}
}
6 changes: 6 additions & 0 deletions Source/SuperLinq.Async/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ than or equal to the given integer.
Determines whether or not the number of elements in the sequence is lesser
than or equal to the given integer.

### BindByIndex

Extracts elements from a sequence according to a a sequence of indices.

This method has 2 overloads.

### Choose

Applies a function to each element of the source sequence and returns a new
Expand Down
124 changes: 124 additions & 0 deletions Source/SuperLinq/BindByIndex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
namespace SuperLinq;

public static partial class SuperEnumerable
{
/// <summary>
/// Selects elements by index from a sequence.
/// </summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="source"/>.</typeparam>
/// <param name="source">The source sequence.</param>
/// <param name="indices">The list of indices of elements in the <paramref name="source"/> sequence to select.</param>
/// <returns>
/// An <see cref="IEnumerable{T}"/> whose elements are the result of selecting elements according to the <paramref name="indices"/> sequence.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="indices"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException">An index in <paramref name="indices"/> is out of range for the input sequence <paramref name="source"/>.</exception>
public static IEnumerable<TSource> BindByIndex<TSource>(
this IEnumerable<TSource> source,
IEnumerable<int> indices)
{
#pragma warning disable MA0015
return BindByIndex(source, indices, static (e, i) => e, static i => throw new ArgumentOutOfRangeException(nameof(indices), "Index is greater than the length of the first sequence."));
#pragma warning restore MA0015
}

/// <summary>
/// Selects elements by index from a sequence and transforms them using the provided functions.
/// </summary>
/// <typeparam name="TSource">The type of the elements of <paramref name="source"/>.</typeparam>
/// <typeparam name="TResult">The type of the elements of the resulting sequence.</typeparam>
/// <param name="source">The source sequence.</param>
/// <param name="indices">The list of indices of elements in the <paramref name="source"/> sequence to select.</param>
/// <param name="resultSelector">A transform function to apply to each source element; the second parameter of the function represents the index of the output sequence.</param>
/// <param name="missingSelector">A transform function to apply to missing source elements; the parameter represents the index of the output sequence.</param>
/// <returns>
/// An <see cref="IEnumerable{T}"/> whose elements are the result of selecting elements according to the <paramref name="indices"/> sequence
/// and invoking the transform function.
/// </returns>
/// <exception cref="ArgumentNullException"><paramref name="source"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="indices"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="resultSelector"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentNullException"><paramref name="missingSelector"/> is <see langword="null"/>.</exception>
/// <remarks>
/// <para>
/// This method uses deferred execution and streams its results.
/// </para>
/// </remarks>
public static IEnumerable<TResult> BindByIndex<TSource, TResult>(
this IEnumerable<TSource> source,
IEnumerable<int> indices,
Func<TSource, int, TResult> resultSelector,
Func<int, TResult> missingSelector)
{
Guard.IsNotNull(source);
Guard.IsNotNull(indices);
Guard.IsNotNull(resultSelector);
Guard.IsNotNull(missingSelector);

return _(source, indices, resultSelector, missingSelector);

static IEnumerable<TResult> _(IEnumerable<TSource> source, IEnumerable<int> indices, Func<TSource, int, TResult> resultSelector, Func<int, TResult> missingSelector)
{
// keeps track of the order of indices to know what order items should be output in
var lookup = indices.Index().ToDictionary(x => { Guard.IsGreaterThanOrEqualTo(x.item, 0, nameof(indices)); return x.item; }, x => x.index);
// keep track of items out of output order
var lookback = new Dictionary<int, TSource>();

// which input index are we on?
var index = 0;
// which output index are we on?
var outputIndex = 0;

// for each item in input
foreach (var item in source)
{
// does the current input index have an output?
if (lookup.TryGetValue(index, out var oi))
{
// is the current item's output order the next one?
if (oi == outputIndex)
{
// return the item and increment output order
yield return resultSelector(item, outputIndex);
outputIndex++;

// while we're here, catch up on any lookbacks
while (lookback.TryGetValue(outputIndex, out var e))
{
yield return resultSelector(e, outputIndex);
lookback.Remove(outputIndex);
outputIndex++;
}
}
// otherwise, store in lookback for later
else
{
lookback[oi] = item;
}
}

index++;
}

// catch up any remaining items
while (outputIndex < lookup.Count)
{
// can we find the current output index in lookback?
if (lookback.TryGetValue(outputIndex, out var e))
{
// return it
yield return resultSelector(e, outputIndex);
lookback.Remove(outputIndex);
}
else
{
// otherwise, return a missing item
yield return missingSelector(outputIndex);
}

outputIndex++;
}
}
}
}
6 changes: 6 additions & 0 deletions Source/SuperLinq/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ the third-last element and so on.

(Obsoleted in favor of Insert)

### BindByIndex

Extracts elements from a sequence according to a a sequence of indices.

This method has 2 overloads.

### Cartesian

Returns the Cartesian product of two or more sequences by combining each
Expand Down
105 changes: 105 additions & 0 deletions Tests/SuperLinq.Async.Test/BindByIndexTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
namespace Test.Async;

public class BindByIndexTest
{
[Fact]
public void BindByIndexIsLazy()
{
new AsyncBreakingSequence<int>().BindByIndex(new AsyncBreakingSequence<int>());
new AsyncBreakingSequence<int>().BindByIndex(new AsyncBreakingSequence<int>(), BreakingFunc.Of<int, int, int>(), BreakingFunc.Of<int, int>());
}

[Fact]
public async Task BindByIndexDisposesEnumerators()
{
await using var seq1 = TestingSequence.Of<int>();
await using var seq2 = TestingSequence.Of<int>();
await seq1.BindByIndex(seq2).AssertEmpty();
}

[Fact]
public Task BindByIndexInOrder()
{
var seq1 = AsyncEnumerable.Range(1, 10);
var seq2 = AsyncSeq(1, 3, 5, 7, 9);

return seq1.BindByIndex(seq2).AssertSequenceEqual(seq2.Select(x => x + 1));
}

[Fact]
public Task BindByIndexOutOfOrder()
{
var seq1 = AsyncEnumerable.Range(1, 10);
var seq2 = AsyncSeq(9, 7, 5, 3, 1);

return seq1.BindByIndex(seq2).AssertSequenceEqual(seq2.Select(x => x + 1));
}

[Fact]
public Task BindByIndexComplex()
{
var seq1 = AsyncEnumerable.Range(1, 10);
var seq2 = AsyncSeq(0, 1, 8, 9, 3, 4, 2);

return seq1.BindByIndex(seq2).AssertSequenceEqual(seq2.Select(x => x + 1));
}

[Theory]
[InlineData(-1)]
[InlineData(10)]
[InlineData(100)]
public Task BindByIndexThrowExceptionInvalidIndex(int index)
{
var seq1 = AsyncEnumerable.Range(1, 10);
var seq2 = AsyncSeq(index);

return Assert.ThrowsAsync<ArgumentOutOfRangeException>("indices",
async () => await seq1.BindByIndex(seq2).Consume());
}

[Fact]
public Task BindByIndexTransformInOrder()
{
var seq1 = AsyncEnumerable.Range(1, 10);
var seq2 = AsyncSeq(1, 3, 5, 7, 9);

return seq1.BindByIndex(seq2, (e, i) => e, i => default(int?)).AssertSequenceEqual(seq2.Select(x => (int?)(x + 1)));
}

[Fact]
public Task BindByIndexTransformOutOfOrder()
{
var seq1 = AsyncEnumerable.Range(1, 10);
var seq2 = AsyncSeq(9, 7, 5, 3, 1);

return seq1.BindByIndex(seq2, (e, i) => e, i => default(int?)).AssertSequenceEqual(seq2.Select(x => (int?)(x + 1)));
}

[Fact]
public Task BindByIndexTransformComplex()
{
var seq1 = AsyncEnumerable.Range(1, 10);
var seq2 = AsyncSeq(0, 1, 8, 9, 3, 4, 2);

return seq1.BindByIndex(seq2, (e, i) => e, i => default(int?)).AssertSequenceEqual(seq2.Select(x => (int?)(x + 1)));
}

[Fact]
public Task BindByIndexTransformInvalidIndex()
{
var seq1 = AsyncEnumerable.Range(1, 10);
var seq2 = AsyncSeq(1, 10, 3, 30);

return seq1.BindByIndex(seq2, (e, i) => e, i => default(int?)).AssertSequenceEqual(2, null, 4, null);
}

[Fact]
public Task BindByIndexTransformThrowExceptionNegativeIndex()
{
var seq1 = AsyncEnumerable.Range(1, 10);
var seq2 = AsyncSeq(-1);

return Assert.ThrowsAsync<ArgumentOutOfRangeException>("indices",
async () => await seq1.BindByIndex(seq2, (e, i) => e, i => default(int?)).Consume());
}
}
Loading

0 comments on commit b942b7f

Please sign in to comment.