Comment utiliser LINQ pour sélectionner un object avec une valeur de propriété minimale ou maximale

J’ai un object Person avec une propriété Nullable DateOfBirth. Existe-t-il un moyen d’utiliser LINQ pour interroger une liste d’objects Person pour celle ayant la valeur DateOfBirth la plus ancienne / la plus petite.

Voici ce que j’ai commencé avec:

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

Les valeurs Null DateOfBirth sont définies sur DateTime.MaxValue afin de les exclure de la considération Min (en supposant qu’au moins un DOB soit spécifié).

Mais tout ce que je fais est de définir firstBornDate à une valeur DateTime. Ce que je voudrais, c’est l’object Person qui correspond à ça. Dois-je écrire une seconde requête comme celle-ci:

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

Ou existe-t-il une façon plus simple de le faire?

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

Malheureusement, il n’y a pas de méthode intégrée pour ce faire.

PM> Package d’installation morelinq

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

Alternativement, vous pouvez utiliser l’implémentation que nous avons dans MoreLINQ , dans MinBy.cs . (Il y a un MaxBy correspondant, bien sûr.) Voici les sortingpes de celui-ci:

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

Notez que cela lancera une exception si la séquence est vide et renverra le premier élément avec la valeur minimale s'il y en a plusieurs.

REMARQUE: j’inclus cette réponse dans son intégralité, car l’OP n’a pas mentionné la source de données et nous ne devrions pas faire d’hypothèses.

Cette requête donne la réponse correcte, mais elle peut être plus lente car elle peut avoir à sortinger tous les éléments dans People , en fonction de la structure de données utilisée par People :

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

MISE À JOUR: En fait, je ne devrais pas appeler cette solution “naïve”, mais l’utilisateur doit savoir contre quoi il interroge. La “lenteur” de cette solution dépend des données sous-jacentes. S’il s’agit d’un tableau ou d’une List , LINQ to Objects n’a pas d’autre choix que de sortinger la totalité de la collection avant de sélectionner le premier élément. Dans ce cas, il sera plus lent que l’autre solution proposée. Toutefois, s’il s’agit d’une table LINQ to SQL et que DateOfBirth est une colonne indexée, SQL Server utilisera l’index au lieu de sortinger toutes les lignes. D’autres implémentations personnalisées IEnumerable pourraient également utiliser des index (voir i4o: Indexed LINQ , ou la firebase database d’objects db4o ) et rendre cette solution plus rapide que Aggregate() ou MaxBy() / MinBy() qui doivent parcourir toute la collection une fois que. En fait, LINQ to Objects aurait pu (en théorie) OrderBy() des cas spéciaux dans OrderBy() pour des collections sortingées comme SortedList , mais ce n’est pas le cas, à ma connaissance.

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

Ferait l’affaire

Donc, vous demandez ArgMin ou ArgMax . C # n’a pas d’API intégrée pour ceux-ci.

Je cherchais une manière propre et efficace (O (n) in time) de le faire. Et je pense en avoir trouvé un:

La forme générale de ce modèle est la suivante:

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

Spécialement, en utilisant l’exemple dans la question originale:

Pour C # 7.0 et versions ultérieures, prend en charge le tuple de valeur :

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

Pour la version C # antérieure à 7.0, le type anonyme peut être utilisé à la place:

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

Ils fonctionnent parce que les deux types tuple et anonyme ont des comparateurs par défaut sensibles: pour (x1, y1) et (x2, y2), ils comparent d’abord x1 vs x2 , puis y1 vs y2 . C’est pourquoi le .Min peut être utilisé sur ces types.

Et comme le type anonyme et le tuple de valeur sont tous deux des types de valeur, ils doivent être très efficaces.

REMARQUE

Dans mes implémentations ArgMin ci-dessus, je suppose que DateOfBirth prend le type DateTime pour plus de simplicité et de clarté. La question d’origine demande d’exclure les entrées avec DateOfBirth champ DateOfBirth nul:

Les valeurs Null DateOfBirth sont définies sur DateTime.MaxValue afin de les exclure de la considération Min (en supposant qu’au moins un DOB soit spécifié).

Il peut être réalisé avec un pré-filtrage

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

Il est donc indifférent de mettre en œuvre ArgMin ou ArgMax .

NOTE 2

L’approche ci-dessus a une mise en garde que lorsque deux instances ont la même valeur min, l’implémentation Min() essaiera de comparer les instances en tant que condition préalable. Cependant, si la classe des instances IComparable pas IComparable , une erreur d’exécution sera IComparable :

Au moins un object doit implémenter IComparable

Heureusement, cela peut encore être corrigé assez proprement. L’idée est d’associer un “ID” de distanct à chaque entrée qui sert de point d’ancrage sans ambiguïté. Nous pouvons utiliser un identifiant incrémentiel pour chaque entrée. Toujours en utilisant l’âge des personnes comme exemple:

 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’a pas vérifié, mais ce doit faire quelque chose prévu:

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

et min:

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

Ne l’ai vérifié que pour l’entité Framework 6.0.0>: Cela peut être fait:

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

Voici la solution la plus générique. Il fait essentiellement la même chose (dans l’ordre O (N)) mais sur tous les types IEnumberable et peut être mélangé avec des types dont les sélecteurs de propriété pourraient renvoyer 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; }); } } 

Tests:

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

EDIT à nouveau:

Pardon. En plus de manquer la nullable, je regardais la mauvaise fonction,

Min <(Of <(TSource, TResult>)>) (IEnumerable <(Of <(TSource>)>)), Func <(Of <(TSource, TResult>)>)) renvoie le type de résultat comme vous l’avez dit.

Je dirais qu’une solution possible est d’implémenter IComparable et d’utiliser Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>)) , qui renvoie vraiment un élément de IEnumerable. Bien sûr, cela ne vous aide pas si vous ne pouvez pas modifier l’élément. Je trouve le design de MS un peu bizarre ici.

Bien sûr, vous pouvez toujours faire une boucle pour le cas échéant, ou utiliser l’implémentation de MoreLINQ que Jon Skeet a fournie.

Je cherchais quelque chose de similaire, de préférence sans utiliser de bibliothèque ni sortinger la liste complète. Ma solution a fini par ressembler à la question elle-même, simplement simplifiée.

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

Pour obtenir le max ou le min d’une propriété à partir d’un tableau d’objects:

Créez une liste qui stocke chaque valeur de propriété:

 list values = new list; 

Ajoutez toutes les valeurs de propriété à lister:

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

Obtenez le max ou le min de la liste:

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

Maintenant, vous pouvez parcourir votre tableau d’objects et comparer les valeurs de propriété que vous voulez vérifier avec les valeurs max ou min int:

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