Order parent collection by minimum values ​​in child collection in Linq

Parent{ List<Child> Children {get;set;} } Child { int Age {get;set;} } 

I would like to order parents with the youngest age of their children, moving to the second or third child in case of a tie.

The closest I came is exactly what the youngest child only orders:

 parents.OrderBy(p => p.Children.Min(c => c.Age)) 

This does not account for the second (or third, etc.) the youngest in the case of a tie.

Given these 3 parents with the appropriate age groups of children, I would like them to come out in that order.

  • P1 1,2,7
  • P2 1,3,6
  • P3 1,4,5
+6
source share
3 answers

What you are trying to do, at a conceptual level, compares the two sequences. Instead of resorting to a special case for this particular sequence, we can simply write a comparator able to compare any two sequences.

He will go through the elements in the sequence to compare the elements in the same position, and then if he finds a pair that is not equal, she knows the result.

 public class SequenceComparer<TSource> : IComparer<IEnumerable<TSource>> { private IComparer<TSource> comparer; public SequenceComparer(IComparer<TSource> comparer = null) { this.comparer = comparer ?? Comparer<TSource>.Default; } public int Compare(IEnumerable<TSource> x, IEnumerable<TSource> y) { return x.Zip(y, (a, b) => comparer.Compare(a, b)) .Where(n => n != 0) .DefaultIfEmpty(x.Count().CompareTo(y.Count())) .First(); } } 

Now we can just use this comparator when calling OrderBy :

 var query = parents.OrderBy(parent => parent.Children .OrderBy(child => child.Age) .Select(child => child.Age) , new SequenceComparer<int>()); 
+3
source

You need to write something like this extension method:

 var orderedParents = parents.OrderBy(p => p.Children, c => c.Age); 

General implementation:

 /// <summary> /// Given a way to determine a collection of elements (for example /// children of a parent) and a comparable property of those items /// (for example age of a child) this orders a collection of elements /// according to the sorting order of the property of the first element /// of their respective collections. In case of a tie, fall back to /// subsequent elements as appropriate. /// </summary> public static IOrderedEnumerable<T> OrderBy<T, TKey, TValue>(this IEnumerable<T> @this, Func<T, IEnumerable<TKey>> getKeys, Func<TKey, TValue> getValue) where TValue : IComparable<TValue> { return @this.OrderBy(x => x, new KeyComparer<T, TKey, TValue>(getKeys, getValue)); } private class KeyComparer<T, TKey, TValue> : IComparer<T> where TValue : IComparable<TValue> { private Func<T, IEnumerable<TKey>> GetKeys; private Func<TKey, TValue> GetValue; public KeyComparer(Func<T, IEnumerable<TKey>> getKeys, Func<TKey, TValue> getValue) { this.GetKeys = getKeys; this.GetValue = getValue; } public int Compare(T x, T y) { var xKeys = GetKeys(x).OrderBy(GetValue).Select(GetValue); var yKeys = GetKeys(y).OrderBy(GetValue).Select(GetValue); foreach (var pair in xKeys.Zip(yKeys, Tuple.Create)) { if (pair.Item1.CompareTo(pair.Item2) != 0) return pair.Item1.CompareTo(pair.Item2); } return xKeys.Count().CompareTo(yKeys.Count()); } } 
+2
source

You can use ThenBy and take 2nd and 3rd children. But it does not scale, so it depends on the needs of impl

If you want something more reliable, you can do the following. He will work in this particular case. I'm going to see if I can optimize it to be more general though :)

 public static class myExt { public static List<Parent> OrderByWithTieBreaker(this List<Parent> parents, int depth = 0) { if (depth > parents[0].Children.Count()) return parents; var returnedList = new List<Parent>(); Func<Parent, int> keySelector = x => { IEnumerable<Child> enumerable = x.Children.OrderBy(y => y.Age).Skip(depth); if (!enumerable.Any()) return 0; //If no children left, then return lowest possible age return enumerable.Min(z => z.Age); }; var orderedParents = parents.OrderBy(keySelector); var groupings = orderedParents.GroupBy(keySelector); foreach (var grouping in groupings) { if (grouping.Count() > 1) { var innerOrder = grouping.ToList().OrderByWithTieBreaker(depth + 1); returnedList = returnedList.Union(innerOrder).ToList(); } else returnedList.Add(grouping.First()); } return returnedList; } } [TestFixture] public class TestClass { public class Parent { public string Name { get; set; } public List<Child> Children { get; set; } } public class Child { public int Age {get;set;} } [Test] public void TestName() { var parents = new List<Parent> { new Parent{Name="P3", Children = new List<Child>{new Child{Age=1}, new Child{Age=3}, new Child{Age=6}, new Child{Age=7}}}, new Parent{Name="P4", Children = new List<Child>{new Child{Age=1}, new Child{Age=3}, new Child{Age=6}, new Child{Age=7}}}, new Parent{Name="P2", Children = new List<Child>{new Child{Age=1}, new Child{Age=3}, new Child{Age=6}}}, new Parent{Name="P1", Children = new List<Child>{new Child{Age=1}, new Child{Age=2}, new Child{Age=7}}}, new Parent{Name="P5", Children = new List<Child>{new Child{Age=1}, new Child{Age=4}, new Child{Age=5}}} }; var f = parents.OrderByWithTieBreaker(); int count = 1; foreach (var d in f) { Assert.That(d.Name, Is.EqualTo("P"+count)); count++; } } 
0
source

All Articles