Cómo usar LINQ para seleccionar un objeto con un valor de propiedad mínimo o máximo

Tengo un objeto Person con una propiedad Nullable DateOfBirth. ¿Hay alguna manera de utilizar LINQ para consultar una lista de objetos Person para el que tenga el valor DateOfBirth más antiguo o más pequeño?

Esto es lo que comencé con:

var firstBornDate = People.Min(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)); 

Los valores nulos de DateOfBirth se establecen en DateTime.MaxValue para descartarlos de la consideración Min (suponiendo que al menos uno tiene una fecha de nacimiento especificada).

Pero todo lo que hace por mí es establecer firstBornDate en un valor DateTime. Lo que me gustaría obtener es el objeto Person que coincida con eso. ¿Debo escribir una segunda consulta así?

 var firstBorn = People.Single(p=> (p.DateOfBirth ?? DateTime.MaxValue) == firstBornDate); 

¿O hay una manera más simple de hacerlo?

 People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) < curMin.DateOfBirth ? x : curMin)) 

Desafortunadamente no hay un método incorporado para hacer esto.

PM> Install-Package morelinq

 var firstBorn = People.MinBy(p => p.DateOfBirth ?? DateTime.MaxValue); 

Alternativamente, puede usar la implementación que tenemos en MoreLINQ , en MinBy.cs . (Hay un MaxBy correspondiente, por supuesto.) Aquí están las agallas:

 public static TSource MinBy(this IEnumerable source, Func selector) { return source.MinBy(selector, null); } public static TSource MinBy(this IEnumerable source, Func selector, IComparer comparer) { if (source == null) throw new ArgumentNullException("source"); if (selector == null) throw new ArgumentNullException("selector"); comparer = comparer ?? Comparer.Default; using (var sourceIterator = source.GetEnumerator()) { if (!sourceIterator.MoveNext()) { throw new InvalidOperationException("Sequence contains no elements"); } var min = sourceIterator.Current; var minKey = selector(min); while (sourceIterator.MoveNext()) { var candidate = sourceIterator.Current; var candidateProjected = selector(candidate); if (comparer.Compare(candidateProjected, minKey) < 0) { min = candidate; minKey = candidateProjected; } } return min; } } 

Tenga en cuenta que esto lanzará una excepción si la secuencia está vacía y devolverá el primer elemento con el valor mínimo si hay más de uno.

NOTA: incluyo esta respuesta para que esté completa ya que el OP no mencionó cuál es la fuente de datos y no deberíamos hacer suposiciones.

Esta consulta proporciona la respuesta correcta, pero podría ser más lenta, ya que podría tener que ordenar todos los elementos en People , dependiendo de la estructura de datos People :

 var oldest = People.OrderBy(p => p.DateOfBirth ?? DateTime.MaxValue).First(); 

ACTUALIZACIÓN: En realidad, no debería llamar a esta solución “ingenua”, pero el usuario necesita saber qué está consultando. La “lentitud” de esta solución depende de los datos subyacentes. Si se trata de una matriz o List , LINQ to Objects no tiene más remedio que ordenar toda la colección primero antes de seleccionar el primer elemento. En este caso, será más lento que la otra solución sugerida. Sin embargo, si se trata de una tabla LINQ a SQL y DateOfBirth es una columna indexada, SQL Server utilizará el índice en lugar de ordenar todas las filas. Otras IEnumerable personalizadas de IEnumerable también podrían hacer uso de índices (vea i4o: Indexed LINQ , o la base de datos de objetos db4o ) y hacer que esta solución sea más rápida que Aggregate() o MaxBy() / MinBy() que necesitan iterar toda la colección una vez. De hecho, LINQ to Objects podría haber (en teoría) creado casos especiales en OrderBy() para colecciones ordenadas como SortedList , pero no es así, hasta donde yo sé.

 People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First() 

Haría el truco

Entonces estás preguntando por ArgMin o ArgMax . C # no tiene una API incorporada para ellos.

He estado buscando una manera limpia y eficiente (O (n) en el tiempo) para hacer esto. Y creo que encontré uno:

La forma general de este patrón es:

 var min = data.Select(x => (key(x), x)).Min().Item2; ^ ^ ^ the sorting key | take the associated original item Min by key(.) 

Especialmente, usando el ejemplo en la pregunta original:

Para C # 7.0 y superior que admite tupla de valor :

 var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2; 

Para la versión de C # anterior a la 7.0, se puede usar el tipo anónimo en su lugar:

 var youngest = people.Select(p => new { ppl = p; age = p.DateOfBirth }).Min().ppl; 

Funcionan porque tanto la tupla de valor como el tipo anónimo tienen comparadores sensibles por omisión: para (x1, y1) y (x2, y2), primero compara x1 contra x2 , luego y1 contra y2 . Es por eso que el .Min se puede usar en esos tipos.

Y dado que tanto el tipo anónimo como la tupla de valor son tipos de valores, ambos deberían ser muy eficientes.

NOTA

En mis implementaciones anteriores de ArgMin , asumí DateOfBirth para tomar el tipo DateTime por simplicidad y claridad. La pregunta original pide excluir aquellas entradas con nulo campo DateOfBirth :

Los valores nulos de DateOfBirth se establecen en DateTime.MaxValue para descartarlos de la consideración Min (suponiendo que al menos uno tiene una fecha de nacimiento especificada).

Se puede lograr con un prefiltrado

 people.Where(p => p.DateOfBirth.HasValue) 

Por lo tanto, es irrelevante la cuestión de implementar ArgMin o ArgMax .

NOTA 2

El enfoque anterior tiene la advertencia de que cuando hay dos instancias que tienen el mismo valor mínimo, la implementación Min() intentará comparar las instancias como un desempate. Sin embargo, si la clase de las instancias no implementa IComparable , se generará un error de tiempo de ejecución:

Al menos un objeto debe implementar IComparable

Afortunadamente, esto aún se puede arreglar bastante limpiamente. La idea es asociar un “ID” lejano con cada entrada que sirva como desempate inequívoco. Podemos usar una identificación incremental para cada entrada. Sigue usando la edad de las personas como ejemplo:

 var youngest = Enumerable.Range(0, int.MaxValue) .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3; 
 public class Foo { public int bar; public int stuff; }; void Main() { List fooList = new List(){ new Foo(){bar=1,stuff=2}, new Foo(){bar=3,stuff=4}, new Foo(){bar=2,stuff=3}}; Foo result = fooList.Aggregate((u,v) => u.bar < v.bar ? u: v); result.Dump(); } 

No lo compré, pero esto debe hacer lo esperado:

 var itemWithMaxValue = SomeListOfClass.OrderByDescending(i => i.SomeFloat).FirstOrDefault(); 

y min:

 var itemWithMinValue = SomeListOfClass.OrderByDescending(i => i.SomeFloat).LastOrDefault(); 

Solo lo he comprobado para Entity framework 6.0.0>: Esto se puede hacer:

 var MaxValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMaxValueFrom).Max(); var MinValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMinValueFrom).Min(); 

La siguiente es la solución más genérica. Básicamente hace lo mismo (en orden O (N)) pero en cualquier tipo IEnumberable y puede mezclarse con tipos cuyos selectores de propiedad podrían devolver nulo.

 public static class LinqExtensions { public static T MinBy(this IEnumerable source, Func selector) { if (source == null) { throw new ArgumentNullException(nameof(source)); } if (selector == null) { throw new ArgumentNullException(nameof(selector)); } return source.Aggregate((min, cur) => { if (min == null) { return cur; } var minComparer = selector(min); if (minComparer == null) { return cur; } var curComparer = selector(cur); if (curComparer == null) { return min; } return minComparer.CompareTo(curComparer) > 0 ? cur : min; }); } } 

Pruebas:

 var nullableInts = new int?[] {5, null, 1, 4, 0, 3, null, 1}; Assert.AreEqual(0, nullableInts.MinBy(i => i));//should pass 

EDITAR de nuevo:

Lo siento. Además de perder el nullable que estaba buscando en la función incorrecta,

Min <(Of <(TSource, TResult>)>) (IEnumerable <(Of <(TSource>)>), Func <(Of <(TSource, TResult>)>)) devuelve el tipo de resultado como usted dijo.

Yo diría que una posible solución es implementar IComparable y usar Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>)) , que realmente devuelve un elemento de IEnumerable. Por supuesto, eso no te ayuda si no puedes modificar el elemento. Encuentro que el diseño de MS es un poco raro aquí.

Por supuesto, siempre puede hacer un ciclo for si lo necesita, o usar la implementación MoreLINQ que dio Jon Skeet.

Estaba buscando algo similar, preferiblemente sin usar una biblioteca ni ordenar toda la lista. Mi solución terminó parecida a la pregunta en sí, simplemente se simplificó un poco.

 var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth)); 

Para obtener el máximo o el mínimo de una propiedad de una matriz de objetos:

Haga una lista que almacene el valor de cada propiedad:

 list values = new list; 

Agregue todos los valores de propiedad a la lista:

 foreach (int i in obj.desiredProperty) { values.add(i); } 

Obtenga el máximo o el mínimo de la lista:

 int Max = values.Max; int Min = values.Min; 

Ahora puede recorrer su matriz de objetos y comparar los valores de la propiedad que desea comparar con el máximo o mínimo int:

 foreach (obj o in yourArray) { if (o.desiredProperty == Max) {return o} else if (o.desiredProperty == Min) {return o} }