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

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

Aqui está o que eu comecei com:

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 Min (supondo que pelo menos um tenha um DOB especificado).

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

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

Ou há uma maneira mais enxuta de fazer isso?

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

Infelizmente não há um método interno para fazer isso.

PM> instalar-pacote morelinq

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

Alternativamente, você pode usar a implementação que temos em MoreLINQ , em MinBy.cs . (Há um MaxBy correspondente, claro.) Aqui estão as entranhas:

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

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

NOTA: Eu incluo esta resposta para a integralidade, pois o OP não mencionou qual é a fonte de dados e não devemos fazer nenhuma suposição.

Essa consulta fornece a resposta correta, mas pode ser mais lenta, pois pode precisar classificar todos os itens em 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 com o que está fazendo uma consulta. A lentidão desta solução depende dos dados subjacentes. Se esta for uma matriz ou List , o LINQ to Objects não tem outra escolha a não ser classificar a coleção inteira antes de selecionar o primeiro item. Neste caso, será mais lento que a outra solução sugerida. No entanto, se esta for uma tabela LINQ to SQL e DateOfBirth for uma coluna indexada, o SQL Server usará o índice em vez de classificar todas as linhas. Outras implementações IEnumerable customizadas também poderiam fazer uso de índices (consulte i4o: LINQ indexado , ou o database de objects db4o ) e tornar essa solução mais rápida que Aggregate() ou MaxBy() / MinBy() que precisam iterar toda a coleção uma vez. Na verdade, LINQ to Objects poderia ter (em teoria) feito casos especiais em OrderBy() para collections ordenadas como SortedList , mas isso não acontece, até onde eu sei.

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

Faria o truque

Então você está pedindo ArgMin ou ArgMax . C # não tem uma API integrada para eles.

Eu tenho procurado por 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 o C # 7.0 e superior que suporta a tupla de valor :

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

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

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

Eles funcionam porque ambos os tipos de tuplas e anônimos de valor têm comparadores padrão sensíveis: para (x1, y1) e (x2, y2), primeiro compara x1 x x2 , depois y1 x y2 . É por isso que o .Min pode 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 implementações ArgMin acima, ArgMin que DateOfBirth tipo DateTime para simplificar e esclarecer. A pergunta original pede para excluir as inputs com o campo DateOfBirth nulo:

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

Pode ser conseguido com uma pré-filtragem

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

Portanto, é irrelevante para a questão da implementação de ArgMin ou ArgMax .

NOTA 2

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

Pelo menos um object deve implementar IComparable

Felizmente, isso ainda pode ser corrigido de forma bastante limpa. A ideia é associar um “ID” de distância a cada input que serve como o desempatador inequívoco. Podemos usar um ID incremental para cada input. Ainda usando as pessoas envelhecem como exemplo:

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

Não cheque, mas isso deve fazer a coisa esperada:

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

e min:

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

Só verifiquei isso para o Entity Framework 6.0.0>: Isso pode ser feito:

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

A seguir, a solução mais genérica. Ele essencialmente faz a mesma coisa (na ordem O (N)), mas em qualquer tipo IEnumberable e pode ser misturado com os tipos cujos seletores de propriedade poderiam retornar null.

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

Testes:

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

EDITAR novamente:

Desculpa. Além de perder o anulável eu estava olhando para a function 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. Claro, isso não ajuda se você não puder modificar o elemento. Acho o design do MS um pouco estranho aqui.

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

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

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

Para obter o máximo ou mínimo de uma propriedade de uma matriz de objects:

Faça uma lista que armazene cada valor de propriedade:

 list values = new list; 

Adicione todos os valores de propriedade à lista:

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

Obtenha o máximo ou o mínimo da lista:

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

Agora você pode percorrer a sua matriz de objects e comparar os valores da propriedade que deseja verificar no int max ou min:

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