Linq GroupBy objects with an if else condition

Viewed 153

I'm new to c# linq and struggling with something. I have a list and want to gather from it specific info as in the following example:

Lets say I have the following list of kidsEat objects:

{NAME=joseph, FOOD=banana, EATEN=true}
 
{NAME=joseph, FOOD=apple, EATEN=false}

{NAME=billy, FOOD=banana, EATEN=false}

{NAME=billy, FOOD=apple, EATEN=false}

From this list, I want to know for each of the boys if he has eaten anything or not.

If he did eat something, it will take the object that says he ate.

If he did not eat, it will take one random object where he didn't eat.

So, this example should return list of following 2 objects:

{NAME=joseph, FOOD=banana, EATEN=true}

{NAME=billy, FOOD=banana, EATEN=false}     //banana could be switched to apple, it doesn't matter

So. I thought of something like:

KidsList.GroupBy(kid=>kid.NAME).Where().Select(kid=>kid.First())

But I don't know what to put in the where clause, because its like going over all rows, then finding if one of them is true, and if so putting "true" else putting false. It feels like it needs some if else inside the LINQ query.

Any help?

4 Answers

Select either first 'eaten' record, or first 'not eaten' record if there are no 'eaten' records

KidsList.GroupBy(k => k.NAME)
  .Select(g => g.FirstOrDefault(k => k.EATEN) ?? g.First())

Explanation: If there are no records matching k => k.EATEN predicate then FirstOrDefault will return the default value, which is null. In this case null-coalescing operator ?? will evaluate the right-hand operand and return the First record (which obviously will be 'not eaten').

You could order the group elements by EATEN and then pick the first element:

var ans = src.GroupBy(e => e.NAME)
             .Select(eg => eg.OrderByDescending(e => e.EATEN).First());

Requirement:

From this list, I want to know for each of the boys if he has eaten anything or not. If he did eat something, it will take the object that says he ate. If he did not eat, it will take one random object where he didn't eat.

What if one of the boys ate more than one item? What does it mean: "take the object that says he ate"?
So let's change this into: "Take any of the objects that he ate".
If you are certain that no one ate more than one item, then there is no difference.

For this you don't need a Where.

First you need to know for every kid the objects that he has eaten and not has eaten. Order this in Descending value for Eaten: first the Foods that were Eaten (=true), then the Foods that were not Eaten (=false). From this sequence of Foods, take the first item.

If the kid has eaten at least one thing, then the first item will be an Eaten item. If he hasn't eaten anything (= if all occurrences of Eaten were false), then the first item will be a not-eaten item.

We'll use the overload of Enumerable.GroupBy that has a parameter resultSelector. In the resultSelector we'll do the OrderByDescending value of Eaten as described above. Then we'll select the first Food.

IEnumerable<KidsEat> kidsEats = ...
var result = kidsEats.GroupBy(kid => kid.Name,

    // parameter resultSelector: for every kid's Name, and all kidsEats with this Name
    // make one new object:
    (name, kidsEatsWithThisName) => new
    {
        Name = name,
        Food = kidsEatsWithThisName
               .OrderByDescending(kidsEat => kidsEat.Eaten)
               .Select(kidsEat => kidsEat.Food)
               .FirstOrDefault(),
    });

In words: from the sequence of kidsEats, make groups of kidsEats with the same name. From every group make one new object, with properties Name and Food as follows:

  • The value of property Name is the key of the group, which is the common name of all kidsEats in this group.
  • To calculate the value of property Food, order all KidsEats in the group (= all KidsEats with this name) by descending value of property Eaten: first the true values, then the false values.
  • From this ordered sequence of KidsEats take the value of property Food. Result: a sequence of Foods, first the Foods that were Eaten by the kid with this name, then the Foods that were not Eaten by this kid.
  • From this sequence of Foods take the first item. If this kid hasn't eaten anything, then the first item is a non-eaten Food. If he has eaten anything, then this first item is an Eaten Food.

Simple comme bonjour! (once you know how to do it)

One way would be to use the Aggregate function.

list.GroupBy(k => k.NAME)
  .Select(g => g.Aggregate ((x, y) => x.EATEN && !y.EATEN ? x : y))
Related