Stécy
Stécy

Reputation: 12339

Is there a better way of calling LINQ Any + NOT All?

I need to check if a sequence has any items satisfying some condition but at the same time NOT all items satisfying the same condition.

For example, for a sequence of 10 items I want to have TRUE if the sequence has at least one that satisfy the condition but not all:

I know I could to this:

mySequence.Any (item => item.SomeStatus == SomeConst) && !mySequence.All (item => item.SomeStatus == SomeConst)

But this is not optimal.

Is there a better way?

Upvotes: 23

Views: 5285

Answers (8)

Robert McKee
Robert McKee

Reputation: 21487

That is likely a fairly optimal solution if the source is a database. This extension method might be better depending on your source (I think, I just threw it together -- likely many many errors, consider it more pseudo code). The benefit here is it only enumerates once and does a short-circuit as soon as it has read enough to determine the outcome:

static bool SomeButNotAll<TSource>(this IEnumerable<TSource> source,
                                   Func<TSource, bool> predicate)
{
   using(var iter=source.GetEnumerator())
   {
     if (iter.MoveNext())
     {
       bool initialValue=predicate(iter.Current);
       while (iter.MoveNext())
         if (predicate(iter.Current)!=initialValue)
           return true;
     }
   }     
   return false; /* All */
}

Upvotes: 3

Jon Hanna
Jon Hanna

Reputation: 113242

If you wanted to define this as a method, you could take then approach Linq takes in defining both IEnumerable<T> and IQueryable<T> extension methods. This allows for the optimal approach to be taken automatically:

public static bool SomeButNotAll<T>(this IQueryable<T> source, Expression<Func<T, bool>> predicate)
{
  if(source == null)
    throw new ArgumentNullException("source");
  if(predicate == null)
    throw new ArgumentNullException("predicate");
  return source.
    Select(predicate)
    .Distinct()
    .Take(2)
    .Count() == 2;
}
public static bool SomeButNotAll<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
  if(source == null)
    throw new ArgumentNullException("source");
  if(predicate == null)
    throw new ArgumentNullException("predicate");
  using(var en = source.GetEnumerator())
    if(en.MoveNext())
    {
      bool first = predicate(en.Current);
      while(en.MoveNext())
        if(predicate(en.Current) != first)
          return true;
    }
  return false;
}

If you're using EntityFramework (or another provider that provides a CountAsync you can also provide an asynchronous version easily:

public static async Task<bool> SomeButNotAllAsync<T>(this IQueryable<T> source, Expression<Func<T, bool>> predicate, CancellationToken cancel)
{
  if(source == null)
    throw new ArgumentNullException("source");
  if(predicate == null)
    throw new ArgumentNullException("predicate");
  cancel.ThrowIfCancellationRequested();
  return await source.
    Select(predicate)
    .Distinct()
    .Take(2)
    .CountAsync(cancel)
    .ConfigureAwait(false) == 2;
}
public static Task<bool> SomeButNotAllAsync<T>(this IQueryable<T> source, Expression<Func<T, bool>> predicate)
{
  return source.SomeButNotAllAsync(predicate, CancellationToken.None);
}

Upvotes: 1

Sophie Swett
Sophie Swett

Reputation: 3390

You could define your own extension method. This version is more verbose, but still readable, and it only enumerates the IEnumerable once:

bool AnyButNotAll<ItemT>(this IEnumerable<ItemT> sequence, Func<ItemT, bool> predicate)
{
    bool seenTrue = false;
    bool seenFalse = false;

    foreach (ItemT item in sequence)
    {
        bool predResult = predicate(item);
        if (predResult)
            seenTrue = true;
        if (!predResult)
            seenFalse = true;

        if (seenTrue && seenFalse)
            return true;
    }

    return false;
}

Much shorter, but enumerates the IEnumerable twice:

bool AnyButNotAll<ItemT>(this IEnumerable<ItemT> sequence, Func<ItemT, bool> predicate)
{
    return sequence.Any(predicate) && !sequence.All(predicate);
}

Upvotes: 3

Sophie Swett
Sophie Swett

Reputation: 3390

You could put your predicate in a variable so that you don't have to repeat the predicate twice:

Func<MyItemType, bool> myPredicate = item => item.SomeStatus == SomeConst;
if (mySequence.Any(myPredicate) && !mySequence.All(myPredicate))
    ...

Upvotes: 1

BJ Myers
BJ Myers

Reputation: 6813

If your concern is iterating through all of the elements in a large collection, you're okay - Any and All will short-circuit as soon as possible.

The statement

mySequence.Any (item => item.SomeStatus == SomeConst)

will return true as soon as one element that meets the condition is found, and

!mySequence.All (item => item.SomeStatus == SomeConst)

will return true as soon as one element does not.

Since both conditions are mutually exclusive, one of the statements is guaranteed to return after the first element, and the other is guaranteed to return as soon as the first element is found.


As pointed out by others, this solution requires starting to iterate through the collection twice. If obtaining the collection is expensive (such as in database access) or iterating over the collection does not produce the same result every time, this is not a suitable solution.

Upvotes: 6

Maarten
Maarten

Reputation: 22945

You can use the Aggregate method to do both things at once. I would suggest to use an anonymous type for the TAccumulate, containing both counters. After the aggregation you can read both values from the resulting anonymous type.

(I can't type an example, I'm on my phone)

See the documentation here: https://msdn.microsoft.com/en-us/library/vstudio/bb549218(v=vs.100).aspx

Upvotes: 0

Rawling
Rawling

Reputation: 50114

You'll like this.

var anyButNotAll = mySequence
    .Select(item => item.SomeStatus == SomeConst)
    .Distinct()
    .Take(2)
    .Count() == 2;

The Take(2) stops it iterating over any more elements than it has to.

Upvotes: 30

Jacob Lambert
Jacob Lambert

Reputation: 7679

You could try this:

var result = mySequence.Select(item => item.SomeStatus == SomeConst)
                      .Distinct().Count() > 1 ? false : true;

Basically I select true or false for each value, get distinct to only get one of each, then count those.

Upvotes: 2

Related Questions