20

(line of code of interest is the last one, the rest is just for a full representation)

Using the following code, I wanted to take VOTERS until I exceeded the maximum votes needed, but it stops right before reaching that maximum number of votes, so my voters pool has 1 fewer voter than I wanted.

Is there a clean way in LINQ where I could have made it take votes UNTIL it reached the maximum numbers of votes? I know I could add one more voter or do this in a loop but I am curious if there was a good way to do this with LINQ instead.

var voters = new List<Person>
                             {
                                 new Person("Alice", Vote.Yes ),
                                 new Person("Bob", Vote.Yes),
                                 new Person("Catherine", Vote.No),
                                 new Person("Denzel", Vote.Yes),
                                 new Person("Einrich", Vote.Abstain),
                                 new Person("Frederica", Vote.Abstain),
                                 new Person("Goeffried", Vote.Abstain),
                             };
            voters.Single(c => c.Name == "Alice").Voices = 100;
            voters.Single(c => c.Name == "Bob").Voices = 150;
            voters.Single(c => c.Name == "Catherine").Voices = 99;
            voters.Single(c => c.Name == "Denzel").Voices = 24;
            voters.Single(c => c.Name == "Einrich").Voices = 52;
            voters.Single(c => c.Name == "Frederica").Voices = 39;
            voters.Single(c => c.Name == "Goeffried").Voices = 99;

// this takes voters until we are BEFORE reaching X voices...
int voicesSoFar = 0;
int voicesNeeded = 300;
var eligibleVoters = voters.TakeWhile((p => (voicesSoFar += p.Voices) < voicesNeeded ));
jason
  • 236,483
  • 35
  • 423
  • 525
PRINCESS FLUFF
  • 3,261
  • 2
  • 23
  • 21

5 Answers5

26

In a situation where I wanted to execute a function until and including it hit an end condition I did:

public static IEnumerable<T> TakeUntilIncluding<T>(this IEnumerable<T> list, Func<T, bool> predicate)
{
    foreach(T el in list)
    {
        yield return el;
        if (predicate(el))
            yield break;
    }
}

Worked for me! I think this is an implementation-agnostic solution like Jason's, but simpler.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
  • 1
    And no external/captured state variable. – Tormod Apr 09 '15 at 13:26
  • 1
    This is a pretty nice extension, it will unquestionably be added to my extensions library. I would just change its name to `TakeWhileIncludingLast` and modify the `if` clause to `if (!predicate(el))` to make it have a similar behavior to the existing extension `TakeWhile`. – sɐunıɔןɐqɐp Jun 12 '18 at 11:53
  • 1
    This didn't work for me when the element was the first in the list - I get the first two elements. Ended up using this instead: stackoverflow.com/a/3098714/1579626 – sǝɯɐſ Jan 26 '21 at 14:55
18

You're looking for

voters.TakeWhile(p => {
   bool exceeded = voicesSoFar > voicesNeeded ;
   voicesSoFar += p.Voices;
   return !exceeded;
});

If you insist on a one-liner, this will work by comparing the previous value:

voters.TakeWhile(p => (voicesSoFar += p.Voices) - p.Voices < voicesNeeded);
Kobi
  • 135,331
  • 41
  • 252
  • 292
  • Note: keep in mind `voicesSoFar` is not correct by the end on the loop, it is juts an helper variable. – Kobi Feb 11 '10 at 05:27
  • 2
    @Benny Jobigan: The nice thing about such an extension method is not having to resort to such trickery every time such functionality is needed. Encapsulate the desired behavior elsewhere and reuse it when needed. – jason Feb 11 '10 at 05:40
7

Just write your own extension method:

static class IEnumerableExtensions {
    public static IEnumerable<T> TakeUntil<T>(
        this IEnumerable<T> elements,
        Func<T, bool> predicate
    ) {
        return elements.Select((x, i) => new { Item = x, Index = i })
                       .TakeUntil((x, i) => predicate(x.Item))
                       .Select(x => x.Item);
    }

    public static IEnumerable<T> TakeUntil<T>(
        this IEnumerable<T> elements,
        Func<T, int, bool> predicate
    ) {
        int i = 0;
        foreach (T element in elements) {
            if (predicate(element, i)) {
                yield return element;
                yield break;
            }
            yield return element;
            i++;
        }
    }
}

Usage:

var eligibleVoters = voters.TakeUntil(
                         p => (voicesSoFar += p.Voices) >= voicesNeeded
                     );

foreach(var voter in eligibleVoters) {
    Console.WriteLine(voter.Name);
}

Output:

Alice
Bob
Catherine
jason
  • 236,483
  • 35
  • 423
  • 525
  • 1
    That said, your lambda expression that is mutating an outer variable makes me feel yucky. In particular, you can't enumerate `eligibleVoters` twice and see the same results which is just nasty. – jason Feb 11 '10 at 05:15
  • Yeah, I realized that afterward and even started this new question: http://stackoverflow.com/questions/2242371/does-this-code-really-cause-an-access-to-modified-closure-problem As for now, I'm trying to wrap my head around this code, I'm new to this :P – PRINCESS FLUFF Feb 11 '10 at 05:19
  • 1
    @PRINCESS FLUFF: Focus on the second method first; the first just invokes the second in a fancy way. Basically I mimiced the fact that `TakeWhile` has two overloads one that is indexed base and the other that is not. – jason Feb 11 '10 at 05:21
0

Variation of Kobi's answer but demonstrates use of (value, index). index is useful in solving similar issues albeit not OP's.

voters.TakeWhile((value, index) => (voicesSoFar += value.Voices) - value.Voices < voicesNeeded);
BSalita
  • 8,420
  • 10
  • 51
  • 68
0

I was facing the same problem. I have used Union and Skip methods, so to take until it was

   IEnumerable<Something> newSomethings  = somethings.TakeWhile(s => s != stop).Union(new List<Something>(){stop});  

and for skip until

   IEnumerable<Something> newSomethings  = somethings.SkipWhile(s => s != stop).Skip(1);  

There is also Take method, which takes some int of first results.

  • 1
    Your TakeWhile() will add "stop" to the end of a list that doesn't contain "stop". This might be undesirable. – Patrick M Nov 21 '18 at 20:48