Skip to content

Instantly share code, notes, and snippets.

@eouw0o83hf
Last active October 20, 2015 15:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eouw0o83hf/bbfe8d1575ddc55052e0 to your computer and use it in GitHub Desktop.
Save eouw0o83hf/bbfe8d1575ddc55052e0 to your computer and use it in GitHub Desktop.
GetSome(): A Scala-inspired C# IEnumerable Extension
public static class Extensions
{
/// <summary>
/// Grants the ability to peek into an IEnumerable and determine if some
/// (n > 0) items are in the collection *without multiply-enumerating it*
/// </summary>
public static ISomeEnumerable<T> GetSome<T>(this IEnumerable<T> source)
{
return new RepeatableEnumerableWrapper<T>(source);
}
}
/// <summary>
/// Defines the abstraction to peek into an IEnumerable and determine if some
/// (n > 0) items are in the collection *without multiply-enumerating it*
/// </summary>
public interface ISomeEnumerable<out T> : IEnumerable<T>
{
/// <summary>
/// Returns true if there are more than 0 elements in the IEnumerable. Can
/// be executed before, during or after enumeration without incurring a
/// re-enumeration penalty.
/// </summary>
bool Some();
}
/// <summary>
/// Implements peeking into an IEnumerable and determine if some
/// (n > 0) items are in the collection *without multiply-enumerating it*
/// </summary>
public class RepeatableEnumerableWrapper<T> : ISomeEnumerable<T>
{
private readonly IEnumerator<T> _inputEnumerator;
// The method that gets fed to the output enumerable executes
// a value immediately, so we store it as a Lazy<> to defer
// all execution since that's idiomatic to IEnumerables. Also,
// by storing this to a field, it verifies that all attempts
// to enumerate this class fall to the same underlying IEnumerable.
private readonly Lazy<IEnumerable<T>> _outputEnumerable;
private bool? _someResult;
public RepeatableEnumerableWrapper(IEnumerable<T> input)
{
if (input == null)
{
_someResult = false;
return;
}
_inputEnumerator = input.GetEnumerator();
_outputEnumerable = new Lazy<IEnumerable<T>>(Enumerate);
}
/// <summary>
/// Pulls the first result off of the enumerator and caches it. If
/// an a result was previously reached, uses that cached result instead.
/// </summary>
/// <returns></returns>
public bool Some()
{
// Guarantees that this method will never run twice for one instance.
// Also, this is set to false in the constructor if the input is null,
// so that edge case is handled.
if (_someResult.HasValue)
{
return _someResult.Value;
}
// Moves the enumerator onto the IEnumerable, at position 0. Returns
// true if the move was successful (and there are > 0 elements in the
// IEnumerable) and false if none.
_someResult = _inputEnumerator.MoveNext();
// Now _inputEnumerator is ready to be moved to the next element, but
// we'll defer that to the actual enumeration down in Enumerate()
return _someResult.Value;
}
private IEnumerable<T> Enumerate()
{
// This call is guaranteed to either (a) fall off the end of the
// IEnumerable [in which case we just quit because there are 0 items]
// or (b) move to the first element.
if (!Some())
{
yield break;
}
// The enumerator is already pointing at the first element, so
// just return it without iterating.
yield return _inputEnumerator.Current;
// Now we're in the happy case of just enumerating the rest of the IEnumerable.
// Keep going until we fall off the end and MoveNext() returns false.
while (_inputEnumerator.MoveNext())
{
yield return _inputEnumerator.Current;
}
}
public IEnumerator<T> GetEnumerator()
{
return _outputEnumerable.Value.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable) _outputEnumerable.Value).GetEnumerator();
}
}
[TestFixture]
public class Tests
{
private class TestEnumerable<T> : IEnumerable<T>
{
public int EnumerationCount { get; private set; }
private readonly IEnumerable<T> _input;
public TestEnumerable(IEnumerable<T> input)
{
_input = input;
EnumerationCount = 0;
}
private IEnumerable<T> Enumerate()
{
++EnumerationCount;
return _input;
}
public IEnumerator<T> GetEnumerator()
{
return Enumerate().GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return Enumerate().GetEnumerator();
}
}
[Test]
public void WhenGivenNullInput_ShouldNotBeSome_ShouldNotExplode()
{
var enumerable = (IEnumerable<object>)null;
var some = enumerable.GetSome();
Assert.That(some, Is.Not.Null);
Assert.That(some.Some(), Is.False);
Assert.Throws<NullReferenceException>(() => some.GetEnumerator());
}
[Test]
public void WhenGivenEmptyInput_ShouldNotBeSome_ShouldSinglyEnumerate()
{
var wrapper = new TestEnumerable<object>(new object[0]);
var some = wrapper.GetSome();
Assert.That(some, Is.Not.Null);
Assert.That(some.Some(), Is.False);
CollectionAssert.IsEmpty(some);
Assert.That(wrapper.EnumerationCount, Is.EqualTo(1));
}
[Test]
public void WhenGivenSingleInput_ShouldBeSome_ShouldSinglyEnumerate()
{
var wrapper = new TestEnumerable<object>(new object[] { 0 });
var some = wrapper.GetSome();
Assert.That(some, Is.Not.Null);
Assert.That(some.Some(), Is.True);
Assert.That(some.Count(), Is.EqualTo(1));
Assert.That(wrapper.EnumerationCount, Is.EqualTo(1));
}
[Test]
public void WhenGivenMultipleInput_ShouldBeSome_ShouldSinglyEnumerate()
{
var wrapper = new TestEnumerable<object>(new object[] { 0, "0", 0.0, 0f, '0', 0m });
var some = wrapper.GetSome();
Assert.That(some, Is.Not.Null);
Assert.That(some.Some(), Is.True);
Assert.That(some.Count(), Is.EqualTo(6));
Assert.That(wrapper.EnumerationCount, Is.EqualTo(1));
}
[Test]
public void WhenGivenMultipleInput_WhenSomeCalledWithinLoop_ShouldBeSome_ShouldSinglyEnumerate()
{
var wrapper = new TestEnumerable<object>(new object[] { 0, "0", 0.0, 0f, '0', 0m });
var some = wrapper.GetSome();
Assert.That(some, Is.Not.Null);
Assert.That(some.Some(), Is.True);
var index = 0;
foreach (var s in some)
{
Assert.That(some.Some());
++index;
}
Assert.That(index, Is.EqualTo(6));
Assert.That(some.Some());
Assert.That(wrapper.EnumerationCount, Is.EqualTo(1));
}
[Test]
public void WhenMultiplyEnumerated_ShouldError()
{
var wrapper = new [] { 0 }.GetSome();
Assert.DoesNotThrow(() => wrapper.ToList());
Assert.Throws<InvalidOperationException>(() => wrapper.ToList());
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment