Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Looking for a particular Enumerable operator sequence: TakeWhile(!) + Concat Single

Tags:

c#

linq

Given an Enumerable, I want to Take() all of the elements up to and including a terminator (throwing an exception if the terminator is not found). Something like:

list.TakeWhile(v => !condition(v)).Concat(list.Single(condition))

..except not crappy. Only want to walk it once.

Is this concisely possible with current operators in .NET 4 and Rx or do I need to write a new operator?

Writing the operator would take me less time than it did to write this question (though I think half that time would be figuring out what to name this function), but I just don't want to duplicate something that's already there.

Update

Ok, here's the operator. Very exciting, I know. Anyway, possible to build it from built-in operators?

    public static IEnumerable<T> TakeThroughTerminator<T>([NotNull] this IEnumerable<T> @this, Func<T, bool> isTerminatorTester)
    {
        foreach (var item in @this)
        {
            yield return item;
            if (isTerminatorTester(item))
            {
                yield break;
            }
        }

        throw new InvalidOperationException("Terminator not found in list");
    }
like image 529
scobi Avatar asked Mar 29 '11 21:03

scobi


People also ask

How to use TakeWhile in c#?

TakeWhile method in C# () With the TakeWhile() method, you can get methods by setting a condition base on Predicate. int[] arr = { 25, 40, 65, 70}; Now, use the TakeWhile() method and predicate to get all the elements that are less than 30.

What is the function of the TakeWhile method?

The TakeWhile<TSource>(IEnumerable<TSource>, Func<TSource,Int32,Boolean>) method tests each element of source by using predicate and yields the element if the result is true . Enumeration stops when the predicate function returns false for an element or when source contains no more elements.

How to use TakeWhile in LINQ?

The TakeWhile Method in Linq is used to fetch all the elements from a data source or sequence until a specified condition is true. Once the condition is failed, then the TakeWhile method will not check the rest of the elements presents in the data source even though the condition is true for the remaining elements.


2 Answers

There isn't a builtin to do such an operation efficiently. It's not very often that people would need to get items that satisfy a condition and one more that doesn't. You'd have to write it yourself.

However you can build this up using existing methods, it just won't be as efficient since you'd need to keep the state somehow only complicating your code. I wouldn't condone this sort of query as it goes against the philosophy of LINQ and would write it myself. But since you asked:

var list = Enumerable.Range(0, 10);
Func<int, bool> condition = i => i != 5;
int needed = 1;
var query = list.Where(item => condition(item)
                                   ? needed > 0
                                   : needed-- > 0)
                .ToList(); // this might cause problems
if (needed != 0)
    throw new InvalidOperationException("Sequence is not properly terminated");

However this has its own problems which can't really be resolved nicely. The right way to deal with this is to code this all by hand (without LINQ). This will give you the exact same behavior.

public static IEnumerable<TSource> TakeWhileSingleTerminated<TSource>(
    this IEnumerable<TSource> source,
    Func<TSource, bool> predicate)
{
    var hasTerminator = false;
    var terminator = default(TSource);
    foreach (var item in source)
    {
        if (!hasFailed)
        {
            if (predicate(item))
                yield return item;
            else
            {
                hasTerminator = true;
                terminator = item;
            }
        }
        else if (!predicate(item))
            throw new InvalidOperationException("Sequence contains more than one terminator");
    }
    if (!hasTerminator)
        throw new InvalidOperationException("Sequence is not terminated");
    yield return terminator;
}

After much thinking about this, I would say it would be difficult to get the most efficient implementation of the original query since it has conflicting requirements. You're mixing TakeWhile() which terminates early with Single() which cannot. It would be possible to replicate the end result (as we all have attempted here) but the behavior cannot without making nontrivial changes to the code. If the goal was to take only the first failing item, then this would be totally possible and replicable, however since it isn't, you'll just have to deal with the problems that this query has.

p.s., I think it's evident how non-trivial this is to do just by how many edits I have made on this answer alone. Hopefully this is my last edit.

like image 64
Jeff Mercado Avatar answered Oct 01 '22 01:10

Jeff Mercado


Here is some hardcore if you don't want to write your own operator:

var input = Enumerable.Range(1, 10);

var condition = new Func<int, bool>(i => i < 5);

bool terminatorPassed = false;
var condition2 = new Func<int, bool>(i =>
        {
            try { return !terminatorPassed; }
            finally { terminatorPassed = !condition(i); }
        });

var result = input.TakeWhile(condition2).ToArray();
if (!terminatorPassed) throw new FutureException("John Connor survived");
like image 31
Snowbear Avatar answered Oct 01 '22 02:10

Snowbear