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


Tengo un objeto Person con una propiedad DateOfBirth anulable. ¿Hay una 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 empecé:

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

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

Pero todo lo que hace para mí es establecer firstBornDate a un valor DateTime. Lo que me gustaría conseguir es el objeto Persona que coincide con eso. Necesito escribir una segunda consulta como esta:

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

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

 370
Author: Chris Marisic, 2009-05-27

12 answers

People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
 249
Author: Paul Betts,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-11-24 10:31:06

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

PM > Install-Package morelinq

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

Alternativamente, puedes usar la implementación que tenemos en MoreLINQ, en MinBy.cs . (Hay un MaxBy correspondiente, por supuesto. Aquí están las entrañas de la misma:

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;
    }
}

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.

 189
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-12 20:38:02

NOTA: Incluyo esta respuesta para completar ya que el OP no mencionó cuál es la fuente de datos y no deberíamos hacer ninguna suposición.

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

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 otra opción 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 DateOfBirth es una columna indexada, entonces SQL Server usará el índice en lugar de ordenar todas las filas. Otras implementaciones personalizadas IEnumerable<T> también podrían hacer uso de índices (ver i4o: Indexed LINQ , o el objeto database 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 (en teoría) haber hecho casos especiales en OrderBy() para colecciones ordenadas como SortedList<T>, pero no lo hace, por lo que sé.

 103
Author: Lucas,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-27 18:28:17
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Haría el truco

 58
Author: Rune FS,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-09-20 18:33:57

Así que estás pidiendo ArgMin o ArgMax. C# no tiene una API integrada para ellos.

He estado buscando una manera limpia y eficiente (O(n) en el 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 soporta 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 predeterminados sensibles: para (x1, y1) y (x2, y2), primero compara x1 vs x2, luego y1 vs y2. Es por eso que el built-in .Min se 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 implementaciones anteriores ArgMin asumí DateOfBirth tomar el tipo DateTime para simplicidad y claridad. La pregunta original pide excluir aquellas entradas con el campo null DateOfBirth:

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

Se puede lograr con un pre-filtrado

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

Así que es irrelevante para la cuestión de implementar ArgMin o ArgMax.

NOTA 2

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

Al menos un objeto debe implementar IComparable

Afortunadamente, esto todavía se puede arreglar bastante limpiamente. La idea es asociar un "ID" distante con cada entrada que sirve como el desempate inequívoco. Podemos utilice un ID incremental para cada entrada. Todavía usando la edad de la gente como ejemplo:

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;
 11
Author: KFL,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-27 18:05:44
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();
}
 4
Author: JustDave,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-06-23 13:01:02

No comprobé, pero esto debe hacer lo esperado:

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

Y min:

var itemWithMinValue = SomeListOfClass.OrderByDescending(i => i.SomeFloat).LastOrDefault();
 2
Author: Andrew,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-01-02 03:24:38

Solo ha comprobado esto 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();
 2
Author: netfed,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-30 01:01:22

La siguiente es la solución más genérica. Esencialmente hace lo mismo (en orden O(N)) pero en cualquier tipoEnumberable y puede mezclarse con tipos cuyos selectores de propiedades podrían devolver null.

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
 2
Author: Zafar Ameem,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-06-16 20:57:11

EDITAR de nuevo:

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

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

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

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

 0
Author: Matthew Flaschen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-05-27 08:23:16

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

var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));
 0
Author: Are,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-26 08:59:33

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

Haga una lista que almacene el valor de cada propiedad:

list<int> values = new list<int>;

Agregue todos los valores de propiedad a la lista:

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

Obtenga el máximo o 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 propiedad que desea verificar con el int max o min:

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

    else if (o.desiredProperty == Min)
        {return o}
}
 -2
Author: David Edel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-05-13 12:34:16