Skip to content

Instantly share code, notes, and snippets.

@franzalex
Last active April 14, 2021 15:18
Show Gist options
  • Save franzalex/ea57f2d07b8b3fbfb8c23b08d242172c to your computer and use it in GitHub Desktop.
Save franzalex/ea57f2d07b8b3fbfb8c23b08d242172c to your computer and use it in GitHub Desktop.
Peek Enumerator for C#
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
/// <summary>
/// Extends the default <see cref="IEnumerator{T}" /> implementation to allow accessing the next
/// element of the collection without advancing the position of the enumerator
/// </summary>
/// <typeparam name="T">The type of objects to enumerate.</typeparam>
/// <seealso cref="System.Collections.Generic.IEnumerator{T}" />
public struct PeekEnumerator<T> : IEnumerator<T>
{
private const string wasReset_ExceptionMessage = "Enumeration has not started. Call MoveNext.";
private IEnumerator<T> _enumerator;
private Queue<T> cache;
private bool wasReset;
/// <summary>Initializes a new instance of the <see cref="PeekEnumerator{T}" /> struct.</summary>
/// <param name="collection">
/// The collection for which a <see cref="PeekEnumerator{T}" /> is to be created.
/// </param>
public PeekEnumerator(IEnumerable<T> collection) : this(collection.GetEnumerator()) { }
/// <summary>Initializes a new instance of the <see cref="PeekEnumerator{T}" /> class.</summary>
/// <param name="enumerator">The enumerator.</param>
/// <exception cref="ArgumentNullException">enumerator</exception>
public PeekEnumerator(IEnumerator<T> enumerator)
{
_enumerator = enumerator ?? throw new ArgumentNullException(nameof(enumerator));
cache = new Queue<T>();
wasReset = true;
}
/// <summary>Gets the element in the collection at the current position of the enumerator.</summary>
/// <exception cref="InvalidOperationException">Enumeration has not started. Call MoveNext.</exception>
public T Current
{
get
{
if (wasReset) throw new InvalidOperationException(wasReset_ExceptionMessage);
if (cache.Count == 0) throw new InvalidOperationException("Enumeration already finished.");
return cache.Peek();
}
}
/// <summary>Gets the element in the collection at the current position of the enumerator.</summary>
object IEnumerator.Current => this.Current;
/// <summary>Gets a value indicating whether this instance has next.</summary>
/// <value><c>true</c> if this instance has next; otherwise, <c>false</c>.</value>
public bool HasNext => cache.Count >= 2 || TryFetchAndCache(2);
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting
/// unmanaged resources.
/// </summary>
public void Dispose()
{
_enumerator.Dispose();
}
/// <summary>Advances the enumerator to the next element of the collection.</summary>
/// <returns>
/// <see langword="true" /> if the enumerator was successfully advanced to the next element;
/// <see langword="false" /> if the enumerator has passed the end of the collection.
/// </returns>
public bool MoveNext()
{
wasReset = false;
////// remove the previous Current value
////if (_cache.Count != 0) _cache.Dequeue();
////
////return _cache.Count > 0 || TryFetchAndCache(1);
if (cache.Any()) cache.Dequeue();
var success = TryFetchAndCache(1); //TODO: Conduct tests and remove unused variable
return cache.Any();
}
/// <summary>Gets the next item without advancing the position of the enumerator.</summary>
/// <returns>The next item in the collection.</returns>
/// <exception cref="InvalidOperationException">Cannot peek beyond end of enumeration.</exception>
public T PeekNext()
{
if (wasReset) throw new InvalidOperationException(wasReset_ExceptionMessage);
if (cache.Count < 2 && !TryFetchAndCache(2))
throw new InvalidOperationException("Cannot peek beyond end of enumeration.");
return cache.ElementAt(1);
}
/// <summary>
/// Sets the enumerator to its initial position, which is before the first element in the collection.
/// </summary>
public void Reset()
{
_enumerator.Reset();
cache.Clear();
wasReset = true;
}
/// <summary>Tries to get the next element without advancing the position of the enumerator.</summary>
/// <param name="result">
/// When this method returns, contains the next element in the collection if there are any
/// elements after the current position of the enumerator, or the default value for
/// <typeparamref name="T" /> if there are none.
/// </param>
/// <returns><c>true</c> if the next element was successfully retrieved; else <c>false</c>.</returns>
public bool TryPeekNext(out T result)
{
try
{
// check prevent peeking if enumerator has been previously reset
if (!wasReset && this.TryFetchAndCache(2))
{
result = this.PeekNext();
return true;
}
}
catch (Exception ex) when (ex is InvalidOperationException || /* from TryFetchAndCache and PeekNext */
ex is ArgumentOutOfRangeException /* from Enumerable.ElementAt() */)
{
/* Empty exception handler to catch known exceptions. */
}
result = default(T);
return false;
}
/// <summary>
/// Try to fetch the specified number of elements from the collection and cache the result.
/// </summary>
/// <param name="count">The number of elements to fetch and cache.</param>
/// <returns>
/// <c>true</c> if at least <paramref name="count" /> elements were successfully fetched; else <c>false</c>.
/// </returns>
/// <exception cref="InvalidOperationException">count</exception>
private bool TryFetchAndCache(int count)
{
if (count <= 0) throw new InvalidOperationException(nameof(count) + " must be greater than 0");
while (cache.Count < count && _enumerator.MoveNext())
cache.Enqueue(_enumerator.Current);
return cache.Count >= count;
}
}
using System.Collections.Generic;
public static class PeekEnumeratorExtensions
{
/// <summary>Returns a <see cref="PeekEnumerator{T}" /> that iterates through the collection.</summary>
/// <typeparam name="T">The element type of objects to enumerate.</typeparam>
/// <param name="collection">The collection to be enumerated.</param>
/// <returns>A <see cref="PeekEnumerator{T}" /> that can be used to iterate through the collection.</returns>
public static PeekEnumerator<T> GetPeekEnumerator<T>(this IEnumerable<T> collection)
{
return new PeekEnumerator<T>(collection.GetEnumerator());
}
}
@franzalex
Copy link
Author

Description

An extension of System.Collections.Generic.IEnumerator<T> to allow accessing the next element of the collection without advancing the position of the enumerator.

Usage

int[] nums = new[] {0, 1, 2, 3, 4, 5};
var pe = new PeekEnumerator<int>(nums);

// alternative using extension methods
var pe = nums.GetPeekEnumerator();
	
while (pe.MoveNext())
{
    Console.WriteLine("{0,-10}: {1,5}", "Current", pe.Current);
    Console.WriteLine("{0,-10}: {1,5}", "Has Next", pe.HasNext);
    Console.WriteLine("{0,-10}: {1,5}", "Peek Next", pe.HasNext ? pe.PeekNext().ToString() : "<None>");
    Console.WriteLine("---");
}

pe.Dispose();

// Output:
// -------
//
// Current   :     0
// Has Next  :  True
// Peek Next :     1
// ---
// Current   :     1
// Has Next  :  True
// Peek Next :     2
// ---
// Current   :     2
// Has Next  :  True
// Peek Next :     3
// ---
// Current   :     3
// Has Next  :  True
// Peek Next :     4
// ---
// Current   :     4
// Has Next  :  True
// Peek Next :     5
// ---
// Current   :     5
// Has Next  : False
// Peek Next : <None>
// ---
// 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment