I sometimes need to enumerate the same collection multiple times so I wrote an extension that does this for me. I call it Loop
. It takes one optional parameter startAt
which allows it to start at an offset.
The extension works with enumerators. If it could not MoveNext
then it gets a new enumerator and tries again. If the second try failed then the collection is empty, otherwise it starts from the beginning. It uses a local function that handles the enumerator by resetting it if necessary.
public static IEnumerable<T> Loop<T>(this IEnumerable<T> values, int startAt = 0)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
var moves = 0;
// ReSharper disable once PossibleMultipleEnumeration
var enumerator = values.GetEnumerator();
try
{
while (true)
{
if (TryMoveNext(enumerator, out enumerator))
{
moves++;
if (startAt > 0 && moves <= startAt)
{
continue;
}
yield return enumerator.Current;
}
else
{
yield break;
}
}
}
finally
{
enumerator.Dispose();
}
bool TryMoveNext(IEnumerator<T> currentEnumerator, out IEnumerator<T> newEnumerator)
{
if (currentEnumerator.MoveNext())
{
newEnumerator = currentEnumerator;
return true;
}
else
{
// Get a new enumerator because we took all elements and try again.
currentEnumerator.Dispose();
// ReSharper disable once PossibleMultipleEnumeration
newEnumerator = values.GetEnumerator();
// If we couldn't move after reset then we're done trying because the collection is empty.
return newEnumerator.MoveNext();
}
}
}
I have the feeling like I might have overcomplicated the implementation.
Here are the tests I wrote for it:
[TestClass]
public class enumerableTest
{
[TestMethod]
public void Loop_EmptyCollection_NoneTakes()
{
var numbers = new int[0];
Assert.IsFalse(numbers.Loop().Take(2).Any());
}
[TestMethod]
public void Loop_LessThanAvailable_NoLoop()
{
var numbers = new[] { 1, 2, 3 };
Assert.That.Collection().AreEqual(new[] { 1, 2 }, numbers.Loop().Take(2));
}
[TestMethod]
public void Loop_MoreThanAvailable_OneLoop()
{
var numbers = new[] { 1, 2, 3 };
Assert.That.Collection().AreEqual(new[] { 1, 2, 3, 1 }, numbers.Loop().Take(4));
}
[TestMethod]
public void Loop_TwiceTheAvailable_TwoLoops()
{
var numbers = new[] { 1, 2, 3 };
Assert.That.Collection().AreEqual(new[] { 1, 2, 3, 1, 2, 3, 1, 2 }, numbers.Loop().Take(8));
}
[TestMethod]
public void Loop_MoreThanAvailableWithStartAt_OneLoop()
{
var numbers = new[] { 1, 2, 3 };
Assert.That.Collection().AreEqual(new[] { 2, 3, 1, 2, 3, 1 }, numbers.Loop(1).Take(6));
}
}