Como usar o LINQ para selecionar objeto com valor mínimo ou máximo da propriedade

466

Eu tenho um objeto Person com uma propriedade Nullable DateOfBirth. Existe uma maneira de usar o LINQ para consultar uma lista de objetos Pessoa para aquele com o menor / menor valor DateOfBirth.

Aqui está o que eu comecei:

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

Os valores nulos de DateOfBirth são definidos como DateTime.MaxValue para excluí-los da consideração Mínima (supondo que pelo menos um tenha um DOB especificado).

Mas tudo o que faz por mim é definir firstBornDate como um valor DateTime. O que eu gostaria de obter é o objeto Person que corresponde a isso. Preciso escrever uma segunda consulta assim:

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

Ou existe uma maneira mais enxuta de fazer isso?

slolife
fonte
24
Apenas um comentário no seu exemplo: você provavelmente não deve usar o Single aqui. Seria lançar uma exceção se duas pessoas tiveram a mesma DateOfBirth
Niki
1
Consulte também o stackoverflow.com/questions/2736236/… quase duplicado , que possui alguns exemplos concisos.
goodeye 8/08/13
4
Que característica simples e útil. MinBy deveria estar na biblioteca padrão. Deveríamos enviar uma solicitação de recebimento
Coronel Panic
2
Isto não parecem existir hoje, apenas fornece uma função para escolher o imóvel:a.Min(x => x.foo);
jackmott
4
Para demonstrar o problema: em Python, max("find a word of maximal length in this sentence".split(), key=len)retorna a string 'sentença'. Em C # "find a word of maximal length in this sentence".Split().Max(word => word.Length)calcula que 8 é o maior comprimento de qualquer palavra, mas não dizer-lhe que a palavra mais longa é .
Coronel Panic

Respostas:

299
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
Ana Betts
fonte
16
Provavelmente um pouco mais lento do que apenas implementar o IComparable e usar Min (ou um loop for). Mas +1 para uma solução O (n) linqy.
Matthew Flaschen
3
Além disso, ele precisa ser <curmin.DateOfBirth. Caso contrário, você está comparando um DateTime com uma Pessoa.
Matthew Flaschen
2
Também tenha cuidado ao usar isso para comparar dois horários. Eu estava usando isso para encontrar o último registro de alteração em uma coleção não ordenada. Falhou porque o registro que eu queria terminou com a mesma data e hora.
Simon Gill
8
Por que você faz a verificação supérflua curMin == null? curMinsó poderia ser nullse você estivesse usando Aggregate()uma semente que é null.
Good Nerd Pride
226

Infelizmente, não há um método interno para fazer isso, mas é fácil de implementar por si mesmo. Aqui estão as entranhas:

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

Exemplo de uso:

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

Observe que isso gerará uma exceção se a sequência estiver vazia e retornará o primeiro elemento com o valor mínimo se houver mais de um.

Como alternativa, você pode usar a implementação que temos MoreLINQ , no MinBy.cs . (Há um correspondente MaxBy, é claro.)

Instale via console do gerenciador de pacotes:

PM> Pacote de instalação morelinq

Jon Skeet
fonte
1
Gostaria de substituir o IEnumerator + enquanto com um foreach
ggf31416
5
Não é possível fazer isso facilmente devido à primeira chamada para MoveNext () antes do loop. Existem alternativas, mas são IMO mais confusas.
911 Jon Skeet
2
Embora eu possa retornar o padrão (T) que parece inapropriado para mim. Isso é mais consistente com métodos como First () e a abordagem do indexador do Dictionary. Você poderia facilmente adaptá-lo, se quisesse.
911 Jon Skeet
8
Concordei com a resposta de Paul por causa da solução que não é de biblioteca, mas obrigado por esse código e link para a biblioteca MoreLINQ, que acho que vou começar a usar!
Slolife 27/05/09
1
@HamishGrubijan: ThrowHelper: code.google.com/p/morelinq/source/browse/MoreLinq/…
Jon Skeet
135

OBSERVAÇÃO: incluo esta resposta para que seja completo, pois o OP não mencionou qual é a fonte de dados e não devemos fazer suposições.

Esta consulta fornece a resposta correta, mas pode ser mais lenta, pois pode ser necessário classificar todos os itens People, dependendo da estrutura de dados People:

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

ATUALIZAÇÃO: Na verdade, não devo chamar essa solução de "ingênua", mas o usuário precisa saber contra o que está consultando. A "lentidão" desta solução depende dos dados subjacentes. Se for uma matriz ou List<T>, o LINQ to Objects não terá outra opção a não ser classificar a coleção inteira primeiro antes de selecionar o primeiro item. Nesse caso, será mais lento que a outra solução sugerida. No entanto, se esta for uma tabela LINQ to SQL e DateOfBirthfor uma coluna indexada, o SQL Server usará o índice em vez de classificar todas as linhas. Outras IEnumerable<T>implementações personalizadas também podem usar índices (consulte i4o: LINQ indexado ou o banco de dados de objetos db4o ) e tornar essa solução mais rápida do que Aggregate()ou MaxBy()/MinBy()que precisa repetir a coleção inteira uma vez. De fato, o LINQ to Objects poderia (em teoria) criar casos especiais OrderBy()para coleções classificadas como SortedList<T>, mas isso não acontece, tanto quanto eu sei.

Lucas
fonte
1
Alguém já postou isso, mas aparentemente o excluiu depois que eu comentei como era lenta (e consumia espaço) (O (n log n) velocidade, na melhor das hipóteses, em comparação com O (n) por min). :)
Matthew Flaschen
sim, daí o meu aviso sobre ser a solução ingênua :) no entanto, é absolutamente simples e pode ser usado em alguns casos (pequenas coleções ou se DateOfBirth é uma coluna DB indexada)
Lucas
outro caso especial (que também não existe) é que seria possível usar o conhecimento de orderby e primeiro fazer uma busca pelo valor mais baixo sem classificar.
Rune FS
Classificar uma coleção é uma operação Nlog (N) que não é melhor que a complexidade de tempo linear ou O (n). Se precisarmos apenas de 1 elemento / objeto de uma sequência que seja mínima ou máxima, acho que devemos ficar com a flexibilidade de tempo linear.
Yawar Murtaza
@yawar a coleção já pode estar classificada (indexada mais provável), caso em que você pode ter O (log n)
Rune FS
63
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Faria o truque

Rune FS
fonte
1
Este é ótimo! Eu usei com OrderByDesending (...) .Pegue (1) no meu caso de linq projetion.
Vedran Mandić
1
Este usa classificação, que excede o tempo O (N) e também usa memória O (N).
George Polevoy
@ GeorgePolevoy, que assume que sabemos bastante sobre a fonte de dados. Se a fonte de dados já tiver um índice classificado no campo especificado, isso seria uma constante (baixa) e seria muito mais rápida que a resposta aceita que exigiria percorrer a lista inteira. Se a fonte de dados, por outro lado, é, por exemplo, uma matriz, é claro que você está certo
Rune FS
@RuneFS - você ainda deve mencionar isso em sua resposta porque é importante.
Rog.ap
O desempenho irá arrastá-lo para baixo. Eu aprendi da maneira mais difícil. Se você deseja o objeto com o valor Mín ou Máx, não precisa classificar a matriz inteira. Apenas uma digitalização deve ser suficiente. Veja a resposta aceita ou veja o pacote MoreLinq.
Sau001 31/01/19
35

Então você está pedindo ArgMinouArgMax . O C # não possui uma API interna para esses.

Eu tenho procurado uma maneira limpa e eficiente (O (n) no tempo) de fazer isso. E acho que encontrei um:

A forma geral desse padrão é:

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

Especialmente, usando o exemplo na pergunta original:

Para C # 7.0 e superior que suporta tupla de valor :

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

Para a versão C # anterior à 7.0, o tipo anônimo pode ser usado:

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

Eles funcionam porque ambos, tupla de valor e tipo anônimo, têm comparadores padrão sensíveis: para (x1, y1) e (x2, y2), ele primeiro compara x1 vs x2, então y1vs y2. É por isso que o built-in .Minpode ser usado nesses tipos.

E como o tipo anônimo e a tupla de valor são tipos de valor, eles devem ser muito eficientes.

NOTA

Nas minhas ArgMinimplementações acima , assumiDateOfBirth tipo DateTimepor simplicidade e clareza. A pergunta original pede para excluir essas entradas com DateOfBirthcampo nulo :

Os valores nulos de DateOfBirth são definidos como DateTime.MaxValue para excluí-los da consideração Mínima (supondo que pelo menos um tenha um DOB especificado).

Isso pode ser alcançado com uma pré-filtragem

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

Portanto, é irrelevante para a questão da implementação ArgMin ouArgMax .

NOTA 2

A abordagem acima tem uma ressalva de que, quando houver duas instâncias com o mesmo valor mínimo, a Min()implementação tentará comparar as instâncias como um desempatador. No entanto, se a classe das instâncias não implementarIComparable , será gerado um erro de tempo de execução:

Pelo menos um objeto deve implementar IComparable

Felizmente, isso ainda pode ser corrigido de forma bastante limpa. A idéia é associar um "ID" distante a cada entrada que serve como desempate inequívoco. Podemos usar um ID incremental para cada entrada. Ainda usando as pessoas envelhecem como exemplo:

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;
KFL
fonte
1
Isso não parece funcionar quando o tipo de valor é a chave de classificação. "Pelo menos um objeto deve implementar o IComparable"
liang
1
muito bom! essa deve ser a melhor resposta.
Guido Mocha
@iang sim boa captura. Felizmente, ainda há uma solução limpa para isso. Veja a solução atualizada na seção "Nota 2".
KFL
O Select pode fornecer o ID! var mais jovem = pessoas.Selecione ((p, i) => (p.DataDeDeBento, i, p)). Min (). Item2;
Jeremy
19

Solução sem pacotes extras:

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

Além disso, você pode envolvê-lo em extensão:

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

e neste caso:

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

A propósito ... O (n ^ 2) não é a melhor solução. Paul Betts deu uma solução mais gorda que a minha. Mas a minha ainda é a solução LINQ e é mais simples e mais curta do que outras soluções aqui.

Andrew
fonte
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
fonte
3

Uso perfeitamente simples de agregado (equivalente a dobrar em outros idiomas):

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

A única desvantagem é que a propriedade é acessada duas vezes por elemento de sequência, o que pode ser caro. Isso é difícil de consertar.

david.pfx
fonte
1

A seguir está a solução mais genérica. Ele basicamente faz a mesma coisa (na ordem O (N)), mas em qualquer tipo IEnumberable e pode ser misturado com tipos cujos seletores de propriedades podem retornar nulos.

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

Testes:

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

EDITAR novamente:

Desculpa. Além de perder o anulável, eu estava olhando para a função errada,

Min <(Of <(TSource, TResult>)>) (IEnumerable <(Of <(TSource>)>), Func <(Of <(TSource, TResult>)>)) retorna o tipo de resultado como você disse.

Eu diria que uma solução possível é implementar IComparable e usar Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>)) , que realmente retorna um elemento do IEnumerable. Obviamente, isso não ajuda se você não pode modificar o elemento. Acho o design da MS um pouco estranho aqui.

Obviamente, você sempre pode fazer um loop for se precisar, ou usar a implementação do MoreLINQ que Jon Skeet forneceu.

Matthew Flaschen
fonte
0

Outra implementação, que poderia funcionar com chaves seletoras que permitem valor nulo, e para a coleção do tipo de referência retorna nulo se nenhum elemento adequado for encontrado. Isso pode ser útil para processar os resultados do banco de dados, por exemplo.

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

Exemplo:

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);
Евгений Орлов
fonte
-2

Eu também estava procurando algo semelhante, de preferência sem usar uma biblioteca ou classificar a lista inteira. Minha solução acabou parecida com a pergunta em si, apenas um pouco simplificada.

var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));
Estamos
fonte
Não seria muito mais eficiente obter o valor mínimo antes da sua instrução linq? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...Caso contrário, ele receberá o min repetidamente até encontrar o que você está procurando.
Nieminen