Filtering an array of objects to remove the ones that don't have the greatest value of for a property

Viewed 61

How can I filter an array of objects to remove the ones that don't have the greatest value for Age grouped by IMCB first.

class Program
{
    static void Main(string[] args)
    {
        Container[] containers = buildContainers();

        // how can I get an array of containers that only contains the IMCB with the greatest Age.
        // eg: [ {IMCB = "123456", Age = 3, Name = "third"}, {IMCB = "12345", Age = 4, Name = "fourth"}  ]
    }

    static Container[] buildContainers()
    {
        List<Container> containers = new List<Container>();
        containers.Add(new Container() { IMCB = "123456", Age = 1, Name = "first" });
        containers.Add(new Container() { IMCB = "123456", Age = 3, Name = "third" });
        containers.Add(new Container() { IMCB = "12345", Age = 2, Name = "second" });
        containers.Add(new Container() { IMCB = "123456", Age = 2, Name = "second" });
        containers.Add(new Container() { IMCB = "12345", Age = 4, Name = "fourth" });
        return containers.ToArray();
    }
}

class Container
{
    public string Name { get; set; }
    public string IMCB { get; set; }
    public int Age { get; set; }
}
3 Answers

Since you want to select elements in the source array that have a Property value that matches to the maximum value of that Property in a group of elements that have a common value in another Property, you can:

  • Group the elements in the array specifying the Property that defines a group in this context.
  • Select elements in each Group where the value of another Property matches the maximum value of the same Property in the Group. Here, SelectMany() is used to flatten the results of the selection into a single sequence, otherwise you'd get an IEnumerable<Container>[] instead of a Container[].
  • Return an array (to match the source collection type) of the resulting elements.
  • Extra: the resulting elements may need to be ordered in some way, e.g., by the Property that created the groupings and/or the Property that selects the maximum value in each Group.
// [...]
Container[] containers = buildContainers();
// [...]

var filteredOnMaxAgePerGroup = containers
    .GroupBy(cnt => cnt.IMCB)
    .SelectMany(grp => grp
        .Where(elm => elm.Age == grp.Max(val => val.Age)))
    .ToArray();

To order the results by the Grouping Property (IMCB, here), add OrderBy() before ToArray():

.OrderBy(elm => elm.IMCB)

To order by the Property the defines the maximum value (Age, here), add OrderBy() or OrderByDescending() and ThenBy() or ThenByDescending() or a combination of these, depending on what better fits here, before ToArray():

.OrderBy(elm => elm.Age)
// Or in descending order
.OrderByDescending(elm => elm.Age)
// or, to define a sub-order based on the Group name 
.OrderBy(elm => elm.Age).ThenBy(elm => elm.IMCB)
// or
.OrderByDescending(elm => elm.Age).ThenBy(elm => elm.IMCB)

The answer of @Jimi works, but shorter code is not always better code. For linq2objects you do not want to calculate maximum value for every item in the group, you better take it out of the loop:

var selection = containers
    .GroupBy(cnt => cnt.IMCB)
    .SelectMany(grp =>
    {
        var max = grp.Max(v => v.Age);
        return grp.Where(elm => elm.Age == max);
    })
    .ToArray();

The difference is O(n^2) algorithm vs O(n) and that may be difference of 50 hours of calculation vs 0.5 seconds of calculation.

1       O(n): 0,0000069 seconds O(n^2): 0,0000048 seconds       Test: OK
10      O(n): 0,0000211 seconds O(n^2): 0,0000319 seconds       Test: OK
100     O(n): 0,0000492 seconds O(n^2): 0,0020465 seconds       Test: OK
1000    O(n): 0,0004217 seconds O(n^2): 0,1992285 seconds       Test: OK
10000   O(n): 0,0041992 seconds O(n^2): 19,7042282 seconds      Test: OK
100000  O(n): 0,0405747 seconds O(n^2): 2012,1564200 seconds    Test: OK
1000000 O(n): 0,4202187 seconds O(n^2): did not finish, estimated 200000 seconds

the test code

static void Test(int count)
{
    List<Container> containers = new List<Container>();
    var tmp = buildContainers();

    for (int i = 0; i < count; ++i)
    {
        containers.AddRange(tmp);
    }
    Console.Write(count);
   
    var st = new System.Diagnostics.Stopwatch();
    st.Start();

    var selection = containers
        .GroupBy(cnt => cnt.IMCB)
        .SelectMany(grp =>
        {
            var max = grp.Max(v => v.Age);
            return grp.Where(elm => elm.Age == max);
        })
        .ToArray();

    Console.Write("\tO(n): " + (st.ElapsedTicks / 10000000.0).ToString("0.0000000") + " seconds");

    st = new System.Diagnostics.Stopwatch();
    st.Start();

    var result = containers
        .GroupBy(cnt => cnt.IMCB)
        .SelectMany(grp => grp.Where(elm => elm.Age == grp.Max(v => v.Age)))
        .ToArray();

    st.Stop();
    Console.Write("\tO(n^2): " + (st.ElapsedTicks / 10000000.0).ToString("0.0000000") + " seconds");

    Console.WriteLine("\tTest: " + (result.SequenceEqual(selection) ? "OK" : "ERROR"));
}

static void Main(string[] args)
{
    Test(1);
    Test(10);//warmup
    Console.Clear();
    Test(1);
    Test(10);
    Test(100);
    Test(1000);
    Test(10000);
    Test(100000);
    Test(1000000);

    Console.ReadKey();
}

First, figure out what the greatest age is:

var maxAge = containers.Max( x => x.Age );

Then select non-matching items:

var result = containers.Where( x => x.Age < maxAge );
Related