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?

370
demandé sur Chris Marisic 2009-05-27 09:31:25

12 réponses

People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
249
répondu Paul Betts 2017-11-24 10:31:06

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.

189
répondu Jon Skeet 2016-01-12 20:38:02

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.

103
répondu Lucas 2009-05-27 18:28:17
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

ferait l'affaire

58
répondu Rune FS 2011-09-20 18:33:57

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;
11
répondu KFL 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
répondu JustDave 2015-06-23 13:01:02

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();
2
répondu Andrew 2017-01-02 03:24:38

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();
2
répondu netfed 2017-05-30 01:01:22

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
2
répondu Zafar Ameem 2017-06-16 20:57:11

É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é.

0
répondu Matthew Flaschen 2009-05-27 08:23:16

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));
0
répondu Are 2017-04-26 08:59:33

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}
}
-2
répondu David Edel 2018-05-13 12:34:16