Comment utiliser LINQ pour sélectionner un objet avec une valeur de propriété minimale ou maximale
j'ai un objet de personne avec une date de naissance nulle propriété. Y a-t-il un moyen d'utiliser LINQ pour interroger une liste D'objets-personnes pour celui dont la date de naissance est la plus ancienne/la plus petite.
voilà ce que j'ai commencé avec:
var firstBornDate = People.Min(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue));
les valeurs Nulledate Ofbirth sont définies à DateTime.MaxValue afin de les exclure de la considération minimale (en supposant qu'au moins un d'entre eux ait une date de naissance spécifiée).
mais tout ce qui fait pour moi est définir firstBornDate à une valeur DateTime. Ce que je voudrais faire est la Personne objet qui correspond à ce. Est-ce que je dois écrire une deuxième requête comme ceci:
var firstBorn = People.Single(p=> (p.DateOfBirth ?? DateTime.MaxValue) == firstBornDate);
ou y a-t-il un moyen plus léger de le faire?
12 réponses
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 faire cela.
PM> Install-Package 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.
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;
}
}
noter que cela va jeter une exception si la séquence est vide, et retournera l'élément premier avec la valeur minimale s'il y en a plus d'un.
NOTE: j'inclus cette réponse pour l'exhaustivité puisque L'OP n'a pas mentionné Quelle est la source de données et nous ne devrions pas faire d'hypothèses.
cette requête donne la bonne réponse, mais pourrait être plus lent car il pourrait avoir à trier tous les éléments dans People
, selon quelle structure de données People
est:
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 n'a besoin de savoir ce qu'il est d'interroger. La "lenteur" de cette solution dépend des données sous-jacentes. S'il s'agit d'un tableau ou List<T>
, alors LINQ to Objects n'a pas d'autre choix que de trier toute la collection avant de sélectionner le premier élément. Dans ce cas, il sera plus lent que l'autre solution proposée. Cependant, s'il s'agit d'une table LINQ to SQL et que DateOfBirth
est une colonne indexée, alors SQL Server utilisera l'index au lieu de trier toutes les lignes. Autre les implémentations personnalisées IEnumerable<T>
pourraient également utiliser des index (voir i4o: Indexed LINQ , ou la base de données d'objets db4o ) et rendre cette solution plus rapide que Aggregate()
ou MaxBy()
/ MinBy()
qui doivent itérer l'ensemble de la collection une fois. En fait, LINQ to Objects aurait pu (en théorie) faire des cas spéciaux dans OrderBy()
pour des collections triées comme SortedList<T>
, mais ce n'est pas le cas, autant que je sache.
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-là.
j'ai cherché une façon propre et efficace (O(n) dans le temps) de le faire. Et je pense que j'en ai trouvé un:
La forme générale de ce modèle est:
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 de la question d'origine:
Pour C# 7.0 et au-dessus qui prend en charge valeur tuple :
var youngest = people.Select(p => (p.DateOfBirth, p)).Min().Item2;
Pour la version C# avant 7.0, anonyme de type peut être utilisé à la place:
var youngest = people.Select(p => new { ppl = p; age = p.DateOfBirth }).Min().ppl;
ils fonctionnent parce que les deux valeurs tuple et anonyme type ont des comparateurs par défaut sensible: pour (x1, y1) et (x2, y2), il compare d'abord x1
vs x2
, puis y1
vs y2
. C'est pourquoi le .Min
intégré peut être utilisé sur ces types de.
et comme les tuples anonymous type et value sont tous deux des types de valeur, ils devraient tous deux être très efficaces.
NOTE
dans mon ci-dessus ArgMin
implémentations j'ai supposé DateOfBirth
pour prendre type DateTime
pour la simplicité et la clarté. La question initiale demande d'exclure les entrées avec le champ null DateOfBirth
:
Null DateOfBirth des valeurs DateTime.MaxValue afin de les exclure de la considération minimale (en supposant qu'au moins un d'entre eux ait une date de naissance spécifiée).
Il peut être réalisé avec un pré-filtrage
people.Where(p => p.DateOfBirth.HasValue)
donc c'est indifférent à la question de la mise en œuvre de ArgMin
ou ArgMax
.
NOTE 2
l'approche ci-dessus comporte une mise en garde selon laquelle lorsqu'il y a sont deux instances qui ont la même valeur min, alors l'implémentation Min()
essaiera de comparer les instances comme un casse-cravate. Cependant, si la classe des instances n'implémente pas IComparable
, alors une erreur d'exécution sera lancée:
au moins un objet doit mettre en œuvre IComparable
heureusement, cela peut encore être corrigé assez proprement. L'idée est d'associer une distance "ID" à chaque entrée qui sert à briser les liens sans équivoque. Nous pouvons utiliser un ID incrémental pour chaque entrée. Toujours en prenant l'âge 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<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();
}
N'a pas vérifié, mais cela doit faire la chose attendue:
var itemWithMaxValue = SomeListOfClass.OrderByDescending(i => i.SomeFloat).FirstOrDefault();
et min:
var itemWithMinValue = SomeListOfClass.OrderByDescending(i => i.SomeFloat).LastOrDefault();
ont seulement Coché ceci pour le cadre D'entité 6.0.0 >: ceci peut être fait:
var MaxValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMaxValueFrom).Max();
var MinValue = dbContext.YourDataClass.Select(x => x.ColumnToFindMinValueFrom).Min();
la solution suivante est plus générale. Il fait essentiellement la même chose (dans L'ordre O(N)) mais sur n'importe quels types IEnumberable et peut être mélangé avec des types dont les sélecteurs de propriétés pourraient retourner 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;
});
}
}
Essais:
var nullableInts = new int?[] {5, null, 1, 4, 0, 3, null, 1};
Assert.AreEqual(0, nullableInts.MinBy(i => i));//should pass
ÉDITER à nouveau:
Désolé. En plus de manquer le 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 une solution possible est de mettre en œuvre IComparable et utiliser Min<(Of <(TSource>)>)(IEnumerable<(Of <(TSource>)>)) , ce qui renvoie vraiment un élément du IEnumerable. Bien sûr, cela ne vous aidera pas si vous ne pouvez pas modifier l'élément. Je trouve la conception de MS un peu bizarre ici.
bien sûr, vous pouvez toujours faire une boucle for si vous avez besoin, ou utiliser L'implémentation MoreLINQ Jon Skeet a donné.
je cherchais moi-même quelque chose de similaire, de préférence sans utiliser une bibliothèque ou trier la liste entière. Ma solution terminés similaire à la question elle-même, juste simplifié un peu.
var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));
Pour obtenir la valeur maximale ou minimale d'une propriété à partir d'un tableau d'objets:
faire une liste qui stocke chaque valeur de propriété:
list<int> values = new list<int>;
ajouter toutes les valeurs de propriétés à la liste:
foreach (int i in obj.desiredProperty)
{ values.add(i); }
tirer le max ou min à partir de la liste:
int Max = values.Max;
int Min = values.Min;
Maintenant vous pouvez boucler votre tableau d'objet et comparer les valeurs de la propriété que vous voulez vérifier avec le Max ou le min int:
foreach (obj o in yourArray)
{
if (o.desiredProperty == Max)
{return o}
else if (o.desiredProperty == Min)
{return o}
}