0

Given the following class structure need to extract various members based on custom property value:

class Item{

    public Item(int position, Image image)
    {
        Position = position;
        Image = image;
    }

    public int Position { get; set; }
    public Image Image { get; set; }
    public int Quality { get; set; }
}

class Items : List<Item> {}
class ConsolidatedItems : List<Items> {}

How do I get a list of "best" Items based on Quality joined by Position (in other words group items where Position is the same):

Input

Items1:

Item1: 1, ImageBinary, 100
Item2: 2, ImageBinary, 98
Item3: 3, ImageBinary, 45
Item4: 4, ImageBinary, 66

Items2:
Item1: 1, ImageBinary, 76
Item2: 2, ImageBinary, 80
Item4: 4, ImageBinary, 33

Items3:
Item1: 2, ImageBinary, 76
Item2: 3, ImageBinary, 80
Item4: 4, ImageBinary, 90

Output

BestItems:
Item1: 1, ImageBinary, 100 (from Items1)
Item2: 2, ImageBinary, 98 (from Items1)
Item3: 3, ImageBinary, 80 (from Items3)
Item4: 4, ImageBinary, 90 (from Items4)
Bob Lotz
  • 85
  • 1
  • 10

2 Answers2

2

LINQ

var best = data
    .GroupBy(x => x.Position)
    .Select(x => x.Aggregate(
        (result, item) => item.Quality > result.Quality ? item : result)
    );
  • assuming that data is a collection of Item
  • make sure to add using System.Linq;
  • best is an IEnumerable you can convert it to a list or array if you wish using ToList() or ToArray()

Edit based on the discussion: In case your data is an instance of ConsolidatedItems, i.e. a nested collection, you need to flatten it first using SelectMany:

var best = data
    .SelectMany(x => x)
    .GroupBy(x => x.Position)
    .Select(x => x.Aggregate(
        (result, item) => item.Quality > result.Quality ? item : result)
    );

Details

  1. data contains your entries
  2. GroupBy creates a collection with key being the property or computed value you selected for grouping. You can of course iterate over all its items which will come handy in the next steps.
  3. Select will compute a new entity for each group -- the one with the best quality.
  4. Aggregate is perhaps the most difficult function here. It iterates over the members of a group and keeps one element as the result. The result is initialized with the first element. Then Aggregate moves on over the whole collection and compares each item against the currently best result. At the end it returns the result with the best element.

Best Practice

Please note that your collection classes are not designed well. Have a look here why not to inherit form List.

Isolin
  • 845
  • 7
  • 20
  • I don't think that worked. I am getting error at **x.Position** Error CS1061 'Items' does not contain a definition for 'Position' and no accessible extension method 'Position' accepting a first argument of type 'Items' could be found. – Bob Lotz May 06 '20 at 21:46
  • My top level class is ConsolidatedItems no Items, not sure if that's causing a problem. – Bob Lotz May 06 '20 at 21:52
  • Depends on what you feed into `data`. I assume it to be an `IEnumerable`. Please have a look here: https://dotnetfiddle.net/AVvT67 it works without problems. You can also create a Fiddle with your whole test code to show the error better. – Isolin May 06 '20 at 21:53
  • That's might be the missing part. I'll add IEnumerable... Thanks! – Bob Lotz May 06 '20 at 21:56
  • @BobLotz yes, that is the source of the problem. Could you please be more specific about your data structures? I thought the `ConsolidatedItems` is related to the results. – Isolin May 06 '20 at 21:59
  • The structure is **Item** is the base class. **Items** is a list of **Item** class. **ConsolidatedItems** is list of Items. I am querying the **ConolidatedItems** class - list of lists. – Bob Lotz May 06 '20 at 22:05
  • Ok, but the `Position` property is independent from that, right? – Isolin May 06 '20 at 22:06
  • I edited the answer to include the suggestion of using `.SelectMany(x => x)` in case you have a nested collection. – Isolin May 06 '20 at 22:08
  • Is there a way to get Items class back or convert the result into Items? – Bob Lotz May 07 '20 at 00:17
  • `var result = new Items(best);`, but as I've written, it is not a good practice to inherit from List just for the sake of having a custom class name :) – Isolin May 07 '20 at 00:31
0

Assuming that the Position is already filled in, then you can use SelectMany to flatten your ConsolidatedItems, group them by the position, order the groups by position, and then choose the best quality from each group:

var bestItems = data
    .SelectMany(x => x)
    .GroupBy(x => x.Position)
    .OrderBy(x => x.Key)
    .Select(x => x.OrderByDescending(i=>i.Quality).First());
Robert McKee
  • 21,305
  • 1
  • 43
  • 57
  • Come on, you really want to sort in _N log(N)_ to get the maximum? – Isolin May 06 '20 at 22:20
  • These are very small lists, usually no more than 3-4 so I'm not worried... – Bob Lotz May 06 '20 at 22:29
  • Example data won't ever be large :) But you never know how large is the real data the question relates to (unless explicitly stated, but not the case here) or who will read your answer in the future and try to apply it to a large database. – Isolin May 07 '20 at 00:28
  • For in-memory (LINQ over objects), Insolin's neat aggregate implementation will run quicker in almost every case, but I doubt that it'll work on LINQ over entity framework, where this one will. Feel free to use the aggregate method, or MoreLinq's MaxBy (`.Select(x=>x.MaxBy(i=>i.Quality))`) if your use case dictates it. – Robert McKee May 07 '20 at 01:37