5
\$\begingroup\$

I had a need to do a look-ahead and look-behind when enumerating though and IEnumerable. Here is a classic situation:

foreach(var element in anEnumerable)
{
    //I can get at the current element while iterating (obviously)
    //But sometimes I need to "lookahead" to see what the next element is going to be
    ///or "lookbehind" to see what the last element was.
}

So I thought I'd see if I could encapsulate the logic to do something like this pseudocode:

foreach(var element in anEnumerable.AsSomethingThatLetsMeSeeAheadAndBehind())
{
    element.Current; //the T of whatever anEnumerable<T> was
    element.Lag; //The preceeding T in the enumerable as we enumerate
    element.Lead;  //The next T in the enumerable as we enumerate
    element.Lead(2); //not in this discussion, but it would be neat to look beyond the adjacent element
}

The AsSomethingThatLetsMeSeeAheadAndBehind() extension method is an attempt to make it simple to generate the wrapper around any enumerable of type T, so I built the below implementations of an IEnumerable<T> and IEnumerator<T> from scratch, but there are some code smells:

  1. Needing two separate extension methods to deal with Value Types and Reference Types. I just could not get it to work any other way (even ValueTypes must return null for lead and lag outside the enumerable)
  2. The List<object> _historyin the Enumerator and the MoveNext() code just feels wrong

This code is a direct copy from Linqpad where It was built. If you copy it and run it, you will see it works as expected. The .Dump() in Linqpad is calling a foreach under the hood.

Suggestions on improvements?

void Main()
{
    var l = new List<int> { 1, 2, 3, 4, 5, 6, 7 };
    var p = new List<IEnumerable<int>>() {l.Take(1), l.Take(2), l.Take(3)};
    l.AsLLEStruct().Dump();
    p.AstLLERef().Dump();
}

public static class extns
{
    public static LagLeadEnumerable<Nullable<T>> AsLLEStruct<T>(this IEnumerable<T> enumerable) where T : struct
    {
        return new LagLeadEnumerable<Nullable<T>>(enumerable.Cast<T?>());
    }

    public static LagLeadEnumerable<T> AstLLERef<T>(this IEnumerable<T> enumerable) where T : class
    {
        return new LagLeadEnumerable<T>(enumerable);
    }
}


public class LagLeadEnumerable<T> : IEnumerable<ILagLeader<T>>
{
    IEnumerable<T> _enumer;

    public LagLeadEnumerable(IEnumerable<T> enumerable)
    {
        _enumer = enumerable;
    }

    public IEnumerator<ILagLeader<T>> GetEnumerator()
    {
        return new LagLeadEnumerator<T>(_enumer.GetEnumerator());
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

public class LagLeadEnumerator<T> : IEnumerator<LagLeadEnumerator<T>>, ILagLeader<T>
{
    IEnumerator<T> _enumerator;
    List<object> _history = new List<object>();
    bool _lastMove = false;
    bool _atEnd = false;

    public LagLeadEnumerator(IEnumerator<T> enumerator)
    {
        _enumerator = enumerator;
        Init();
    }

    object IEnumerator.Current
    {
        get
        {
            return this;
        }
    }

    LagLeadEnumerator<T> IEnumerator<LagLeadEnumerator<T>>.Current
    {
        get
        {
            return this;
        }
    }

    public void Dispose()
    {
        _enumerator.Dispose();
    }

    public bool MoveNext()
    {
        _history.Add(_enumerator.Current);
        _lastMove = _atEnd;
        if (!_lastMove)
        {
            _atEnd = !_enumerator.MoveNext();
        }
        return !(_atEnd & _lastMove);
    }

    public void Reset()
    {
        throw new NotImplementedException();
    }

    void Init()
    {
        _history.Clear();
        _enumerator.MoveNext();
        _history.Add(null);
        _history.Add(null);
    }

    public T Lead
    {
        get
        {
            if (!_atEnd)
            {
                return _enumerator.Current;
            }
            return default(T);
        }
    }

    public T Lag
    {
        get
        {
            return (T)_history[_history.Count - 2];
        }
    }


    public T Current
    {
        get
        {
            return (T)_history[_history.Count - 1];
        }
    }

    T ILagLeader<T>.Lead => throw new NotImplementedException();

    T ILagLeader<T>.Lag => throw new NotImplementedException();
}

public interface ILagLeader<T>
{
    T Lead { get; }
    T Current { get; }
    T Lag { get; }
}
\$\endgroup\$
5
  • 1
    \$\begingroup\$ Could explain the look-ahead and look-behind idea in more detail? Even when looking at the code in LINQPad I still don't get it... and you are also not making it easy to understand with the three letter interface ILL ;-| is this any know algorithm? Maybe you could add a link? \$\endgroup\$
    – t3chb0t
    Commented Nov 23, 2017 at 16:34
  • \$\begingroup\$ @t3chb0t I'd also like an explanation. But as far as I can see, the idea is that as you go through the enumerator, you keep track of the three most recent items instead of just the 1. In order these are Lag, Current and Lead. So unlike a usual enumerator, Current doesn't refer to the head of the enumerator but the one before it. I'm still not sure exactly what happens in the edge cases (beginning and end of list)... Would be nice to see the output from those Dumps. \$\endgroup\$ Commented Nov 23, 2017 at 18:47
  • \$\begingroup\$ @BenAaronson I saw the output of all Dumps but I'm not feeling like debugging it ;-) Let's hope OP'll provide more info. \$\endgroup\$
    – t3chb0t
    Commented Nov 23, 2017 at 18:56
  • \$\begingroup\$ Apologies for the confusion. I've updated the question, does that help ? \$\endgroup\$ Commented Nov 24, 2017 at 0:01
  • \$\begingroup\$ For the first point you could implement your own option type(like nullable) that works for both ref and value types. Of course the T? wouldn't be available. This would also allow you to make the history a List<Maybe<T>> instead of List<object>(where Maybe is your own option type, I believe there are some implementations on NuGet). I don't have time right now to provide example code for these, so take these as mere suggestions. \$\endgroup\$
    – Mor A.
    Commented Nov 24, 2017 at 16:45

2 Answers 2

1
\$\begingroup\$

Its a nice idea.. Sometimes I have exactly that problem.

However, I see some improvements in your implementation:

Naming

AsLLEStruct / AsLLERef is not very intuitive. Just take a look at the Enumerable class of the .Net framework and find a name that fits into the naming pattern.

I would call it "EumerateContextAware", "EnumerateWithContext" or something like that... The object to be return may be called EnumerationContext with properties Current, Next, Previous.

Do not use the enumerator as retund value

AsLLEStruct().ToList().Select(a => a.Current).Dump();
// gives {7, 7, 7, 7, 7, 7, 7}

Return a new EnumerationContext object that persist its state.

\$\endgroup\$
1
\$\begingroup\$

Specification

even ValueTypes must return null for lead and lag outside the enumerable

This behavior is in conflict with that of IEnumerator.Current. The value should be undefined if out of bounds.

Current is undefined under any of the following conditions:

  • The enumerator is positioned before the first element in the collection, immediately after the enumerator is created. MoveNext must be called to advance the enumerator to the first element of the collection before reading the value of Current.
  • The last call to MoveNext returned false, which indicates the end of the collection.
  • The enumerator is invalidated due to changes made in the collection, such as adding, modifying, or deleting elements.

What exactly is 'undefined'? Find out with me..

int[]a = new int[] {0};
var  e = a.GetEnumerator();
e.MoveNext();
e.MoveNext();

//  System.InvalidOperationException: Enumeration already finished.
Console.WriteLine(e.Current);  

This means that lag and lead should also throw an InvalidOperationException when out of bounds. So there is no value in differentiating between value and reference types.


Alternative 1

What you could do is augment LagLeadEnumerator<T> with:

  • methods CanLag() and CanLead(), optionally with overloads that take an offset.
  • methods TryLag() and TryLead(), optionally with overloads that take an offset.
  • I also like the names Previous and Next (as suggested in JanDotNet's answer) as aliases for Lag(1) and Lead(1).

Alternative 2

As an alternative you could use a stream with look-around support. Many compiler generator APIs come with such classes. For instance, ANTLRInputStream which provides a method LA(int i).

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.