Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Linq: forward looking condition

Tags:

c#

linq

I have a list (simplified)

[Kind]      [Name]
null        E
null        W
4           T
5           G
6           Q
null        L
null        V
7           K
2           Z
0           F

I need {E,L} -> Items where their Kind==null and the next Kind==null too

Assume that there is an ID that is increasing and in order.

Is this forward looking possible in Linq?

like image 871
Ian Vink Avatar asked Jun 30 '12 19:06

Ian Vink


2 Answers

Like this?

void Main()
{
    List<SomeClass> list = new List<SomeClass>() {
        new SomeClass() { Kind = null, Name = "E" },
        new SomeClass() { Kind = null, Name = "W" },
        new SomeClass() { Kind = 4, Name = "T" },
        new SomeClass() { Kind = 5, Name = "G" },
        ...
    };

    var query = list.Where ((s, i) =>
        !s.Kind.HasValue &&
        list.ElementAtOrDefault(i + 1) != null &&
        !list.ElementAt(i + 1).Kind.HasValue);
}

public class SomeClass
{
    public int? Kind { get; set; }
    public string Name { get; set; }
}

Edit: Stealing @Jeff Marcado's solution to implement an extension method similar to the above use but a bit cleaner and not making you deal with the index:

public static IEnumerable<TSource> WhereWithLookahead<TSource>(this IEnumerable<TSource> source, Func<TSource, TSource, bool> predicate) where TSource : class
{
    using(var enumerator = source.GetEnumerator())
    {
        if (!enumerator.MoveNext())
        {
            //empty
            yield break;
        }

        var current = enumerator.Current;
        while (enumerator.MoveNext())
        {
            var next = enumerator.Current;

            if(predicate(current, next))
            {
                yield return current;
            }

            current = next;
        }

        if (predicate(current, null))
        {
            yield return current;
        }

    }
}

// Use:
var query2 = list.WhereWithLookahead((current, next) =>
    !current.Kind.HasValue &&
    (next != null) &&
    next.Kind.HasValue);
like image 124
Ocelot20 Avatar answered Nov 08 '22 19:11

Ocelot20


For a functional approach, you can implement a lookahead enumerator like so:

IEnumerable<Item> collection = ...;
var lookahead = collection.Zip(collection.Skip(1), Tuple.Create);

The enumerator will iterate through tuples of each item and it's following item. This excludes the last item in the collection. Then it's just a matter of performing the query.

var query = collection.Zip(collection.Skip(1), Tuple.Create)
    .Where(tuple => tuple.Item1.Kind == null && tuple.Item2.Kind == null)
    .Select(tuple => tuple.Item1);

Unfortunately this will be very inefficient. You're enumerating the length of the collection twice and can be very expensive.

It would be better to write your own enumerator for this so you only go through the collection in one pass:

public static IEnumerable<TResult> LookAhead<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, TSource, TResult> selector)
{
    if (source == null) throw new ArugmentNullException("source");
    if (selector == null) throw new ArugmentNullException("selector");

    using (var enumerator = source.GetEnumerator())
    {
        if (!enumerator.MoveNext())
        {
            //empty
            yield break;
        }
        var current = enumerator.Current;
        while (enumerator.MoveNext())
        {
            var next = enumerator.Current;
            yield return selector(current, next);
            current = next;
        }
    }
}

Then the query becomes:

var query = collection.LookAhead(Tuple.Create)
    .Where(tuple => tuple.Item1.Kind == null && tuple.Item2.Kind == null)
    .Select(tuple => tuple.Item1);
like image 26
Jeff Mercado Avatar answered Nov 08 '22 20:11

Jeff Mercado