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

466

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

Esto es con lo que comencé:

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 mínima (suponiendo que al menos uno tenga un DOB especificado).

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

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

¿O hay una forma más ágil de hacerlo?

Slolife
fuente
24
Solo un comentario sobre su ejemplo: probablemente no debería usar Single aquí. Sería una excepción si dos personas tuvieran la misma DateOfBirth
Niki
1
Vea también el stackoverflow.com/questions/2736236/… casi duplicado , que tiene algunos ejemplos concisos.
goodeye
44
Qué característica tan simple y útil. MinBy debería estar en la biblioteca estándar. Deberíamos enviar una solicitud de extracción a Microsoft github.com/dotnet/corefx
Coronel Panic el
2
Esto parece existir hoy, solo proporcione una función para elegir la propiedad:a.Min(x => x.foo);
jackmott
44
Para demostrar el problema: en Python, max("find a word of maximal length in this sentence".split(), key=len)devuelve la cadena 'oración'. En C # "find a word of maximal length in this sentence".Split().Max(word => word.Length)calcula que 8 es la longitud más larga de cualquier palabra, pero no te dice lo que la palabra más larga es .
Coronel Panic

Respuestas:

299
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
Ana Betts
fuente
16
Probablemente un poco más lento que simplemente implementar IComparable y usar Min (o un bucle for). Pero +1 para una solución O (n) linqy.
Matthew Flaschen
3
Además, debe ser <curmin.DateOfBirth. De lo contrario, está comparando una fecha y hora con una persona.
Matthew Flaschen
2
También tenga cuidado al usar esto para comparar dos tiempos de fecha. Estaba usando esto para encontrar el último registro de cambio en una colección desordenada. Falló porque el disco que quería terminaba con la misma fecha y hora.
Simon Gill
8
¿Por qué haces el cheque superfluo curMin == null? curMinsolo podría ser nullsi estuvieras usando Aggregate()una semilla que es null.
Good Night Nerd Pride
226

Desafortunadamente no hay un método incorporado para hacer esto, pero es bastante fácil de implementar por ti mismo. Aquí están las entrañas:

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector)
{
    return source.MinBy(selector, null);
}

public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source,
    Func<TSource, TKey> selector, IComparer<TKey> comparer)
{
    if (source == null) throw new ArgumentNullException("source");
    if (selector == null) throw new ArgumentNullException("selector");
    comparer = comparer ?? Comparer<TKey>.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;
    }
}

Ejemplo de uso:

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

Tenga en cuenta que esto arrojará 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.

Alternativamente, puede usar la implementación que tenemos en MoreLINQ , en MinBy.cs . (Hay un correspondiente MaxBy, por supuesto).

Instalar a través de la consola del administrador de paquetes:

PM> Install-Package morelinq

Jon Skeet
fuente
1
Reemplazaría el Ienumerator + con un foreach
ggf31416
55
No puedo hacer eso fácilmente debido a la primera llamada a MoveNext () antes del bucle. Hay alternativas, pero son IMO más desordenadas.
Jon Skeet
2
Si bien podría devolver el valor predeterminado (T) que me parece inapropiado. Esto es más consistente con métodos como First () y el enfoque del indexador de diccionario. Sin embargo, podrías adaptarlo fácilmente si quisieras.
Jon Skeet
8
Le di la respuesta a Paul debido a la solución que no es de biblioteca, pero gracias por este código y enlace a la biblioteca MoreLINQ, ¡creo que comenzaré a usar!
slolife
1
@HamishGrubijan: ThrowHelper: code.google.com/p/morelinq/source/browse/MoreLinq/…
Jon Skeet
135

NOTA: Incluyo esta respuesta para completar ya que el OP no mencionó cuál es la fuente de datos y no debemos hacer suposiciones.

Esta consulta da la respuesta correcta, pero podría ser más lenta ya que podría tener que ordenar todos los elementos People, según 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 contra qué está consultando. La "lentitud" de esta solución depende de los datos subyacentes. Si se trata de una matriz o List<T>, entonces LINQ to Objects no tiene más remedio que ordenar la colección completa primero antes de seleccionar el primer elemento. En este caso será más lento que la otra solución sugerida. Sin embargo, si esta es una tabla LINQ to SQL y DateOfBirthes una columna indexada, SQL Server usará el índice en lugar de ordenar todas las filas. Otros personalizados IEnumerable<T>implementaciones también pueden hacer uso de índices (ver i4o: indexado LINQ , o la base de datos de objetos db4o ) y hacer de esta solución 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 (en teoría) haber hecho casos especiales OrderBy()para colecciones ordenadas como SortedList<T>, pero, por lo que sé, no lo hace.

Lucas
fuente
1
Alguien ya publicó eso, pero aparentemente lo eliminó después de que comenté cuán lento (y lento) era (O (n log n) velocidad en el mejor de los casos en comparación con O (n) por minuto). :)
Matthew Flaschen
Sí, de ahí mi advertencia sobre ser la solución ingenua :) Sin embargo, es muy simple y podría ser utilizable en algunos casos (pequeñas colecciones o si DateOfBirth es una columna de base de datos indexada)
Lucas
Otro caso especial (que tampoco está allí) es que sería posible utilizar el conocimiento de orderby y primero hacer una búsqueda del valor más bajo sin ordenar.
Rune FS
Ordenar una colección es una operación Nlog (N) que no es mejor que la complejidad lineal u O (n). Si solo necesitamos 1 elemento / objeto de una secuencia que sea min o max, creo que deberíamos seguir con la complejidad del tiempo lineal.
Yawar Murtaza
@yawar, la colección ya podría estar ordenada (indexada más probable) en cuyo caso puede tener O (log n)
Rune FS
63
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Haría el truco

Runa FS
fuente
1
Este es genial! Solía ​​con OrderByDesending (...). Take (1) en mi caso de proyección linq.
Vedran Mandić
1
Éste usa la clasificación, que excede el tiempo O (N) y también usa la memoria O (N).
George Polevoy
@GeorgePolevoy que asume que sabemos bastante sobre la fuente de datos. Si la fuente de datos ya tiene un índice ordenado en el campo dado, esta sería una constante (baja) y sería mucho más rápido que la respuesta aceptada que requeriría recorrer toda la lista. Si, por otro lado, la fuente de datos es, por ejemplo, una matriz, por supuesto, tiene razón
Rune FS
@RuneFS: aún así debe mencionar eso en su respuesta porque es importante.
rory.ap
El rendimiento te arrastrará hacia abajo. Lo aprendí por las malas. Si desea el objeto con el valor Mín. O Máx., No necesita ordenar toda la matriz. Solo 1 escaneo debería ser suficiente. Mire la respuesta aceptada o mire el paquete MoreLinq.
Sau001
35

Entonces estás pidiendo ArgMino ArgMax. C # no tiene una API integrada para esos.

He estado buscando una manera limpia y eficiente (O (n) a tiempo) de 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 la tupla de valor :

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

Para la versión 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;

Trabajan porque ambos tuple de valor y tipo anónimo tienen comparadores sensibles predeterminados: para (x1, y1) y (x2, y2), primero se compara x1frente x2, entonces y1vs y2. Es por eso que el incorporado .Minse puede usar en esos tipos.

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

NOTA

En mis ArgMinimplementaciones anteriores , asumí DateOfBirthque tomaba el tipo DateTimede letra por simplicidad y claridad. La pregunta original pide excluir esas entradas con DateOfBirthcampo nulo :

Los valores nulos de DateOfBirth se establecen en DateTime.MaxValue para descartarlos de la consideración mínima (suponiendo que al menos uno tenga un DOB especificado).

Se puede lograr con un prefiltrado

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

Por lo tanto, no tiene importancia la cuestión de implementar ArgMino ArgMax.

NOTA 2

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

Al menos un objeto debe implementar IComparable

Afortunadamente, esto todavía se puede arreglar de manera bastante limpia. La idea es asociar una "ID" distante con cada entrada que sirva como un desempate inequívoco. Podemos usar una identificación incremental para cada entrada. Todavía 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;
KFL
fuente
1
Esto no parece funcionar cuando el tipo de valor es la clave de clasificación. "Al menos un objeto debe implementar IComparable"
liang
1
¡demasiado bueno! Esta debería ser la mejor respuesta.
Guido Mocha
@liang sí buena captura. Afortunadamente, todavía hay una solución limpia para eso. Consulte la solución actualizada en la sección "Nota 2".
KFL
¡Select puede darle la identificación! var más joven = people.Select ((p, i) => (p.DateOfBirth, i, p)). Min (). Item2;
Jeremy el
19

Solución sin paquetes adicionales:

var min = lst.OrderBy(i => i.StartDate).FirstOrDefault();
var max = lst.OrderBy(i => i.StartDate).LastOrDefault();

También puedes envolverlo en extensión:

public static class LinqExtensions
{
    public static T MinBy<T, TProp>(this IEnumerable<T> source, Func<T, TProp> propSelector)
    {
        return source.OrderBy(propSelector).FirstOrDefault();
    }

    public static T MaxBy<T, TProp>(this IEnumerable<T> source, Func<T, TProp> propSelector)
    {
        return source.OrderBy(propSelector).LastOrDefault();
    }
}

y en este caso:

var min = lst.MinBy(i => i.StartDate);
var max = lst.MaxBy(i => i.StartDate);

Por cierto ... O (n ^ 2) no es la mejor solución. Paul Betts dio una solución más rica que la mía. Pero mi sigue siendo la solución LINQ y es más simple y más corta que otras soluciones aquí.

Andrés
fuente
3
public class Foo {
    public int bar;
    public int stuff;
};

void Main()
{
    List<Foo> fooList = new List<Foo>(){
    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();
}
JustDave
fuente
3

Uso perfectamente simple de agregado (equivalente a doblar en otros idiomas):

var firstBorn = People.Aggregate((min, x) => x.DateOfBirth < min.DateOfBirth ? x : min);

El único inconveniente es que se accede a la propiedad dos veces por elemento de secuencia, lo que podría ser costoso. Eso es difícil de arreglar.

david.pfx
fuente
1

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 propiedades podrían devolver nulo.

public static class LinqExtensions
{
    public static T MinBy<T>(this IEnumerable<T> source, Func<T, IComparable> 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
zafar
fuente
0

EDITAR de nuevo:

Lo siento. Además de perder el anulable, estaba mirando la función incorrecta,

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

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 el diseño de MS un poco extraño aquí.

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

Matthew Flaschen
fuente
0

Otra implementación, que podría funcionar con teclas de selección anulables, y para la recopilación del tipo de referencia devuelve nulo si no se encuentran elementos adecuados. Esto podría ser útil luego de procesar los resultados de la base de datos, por ejemplo.

  public static class IEnumerableExtensions
  {
    /// <summary>
    /// Returns the element with the maximum value of a selector function.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of source.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
    /// <param name="source">An IEnumerable collection values to determine the element with the maximum value of.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
    /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
    /// <returns>The element in source with the maximum value of a selector function.</returns>
    public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, 1);

    /// <summary>
    /// Returns the element with the minimum value of a selector function.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of source.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
    /// <param name="source">An IEnumerable collection values to determine the element with the minimum value of.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
    /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
    /// <returns>The element in source with the minimum value of a selector function.</returns>
    public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, -1);


    private static TSource MaxOrMinBy<TSource, TKey>
      (IEnumerable<TSource> source, Func<TSource, TKey> keySelector, int sign)
    {
      if (source == null) throw new ArgumentNullException(nameof(source));
      if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
      Comparer<TKey> comparer = Comparer<TKey>.Default;
      TKey value = default(TKey);
      TSource result = default(TSource);

      bool hasValue = false;

      foreach (TSource element in source)
      {
        TKey x = keySelector(element);
        if (x != null)
        {
          if (!hasValue)
          {
            value = x;
            result = element;
            hasValue = true;
          }
          else if (sign * comparer.Compare(x, value) > 0)
          {
            value = x;
            result = element;
          }
        }
      }

      if ((result != null) && !hasValue)
        throw new InvalidOperationException("The source sequence is empty");

      return result;
    }
  }

Ejemplo:

public class A
{
  public int? a;
  public A(int? a) { this.a = a; }
}

var b = a.MinBy(x => x.a);
var c = a.MaxBy(x => x.a);
Евгений Орлов
fuente
-2

Estaba buscando algo similar yo mismo, preferiblemente sin usar una biblioteca o ordenar toda la lista. Mi solución terminó similar a la pregunta en sí, simplemente simplificada un poco.

var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));
Son
fuente
¿No sería mucho más eficiente obtener el mínimo antes de su declaración linq? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...De lo contrario, obtendrá el min repetidamente hasta que encuentre el que está buscando.
Nieminen