web-dev-qa-db-fra.com

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

J'ai un objet Personne avec une propriété Nullable DateOfBirth. Existe-t-il un moyen d’utiliser LINQ pour interroger une liste d’objets Personne pour celui avec 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 une d'entre elles possède une DOB spécifiée).

Mais tout ce que je fais est de définir firstBornDate sur une valeur DateTime. Ce que j'aimerais obtenir, c'est l'objet Person qui correspond à cela. Dois-je écrire une seconde requête comme ceci:

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

Ou y a-t-il une manière plus maigre de le faire?

421
slolife
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
278
Paul Betts

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

PM> Paquet 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 bien sûr un MaxBy correspondant.) En voici les entrailles:

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

Notez que cela lève une exception si la séquence est vide et renvoie l'élément en premier avec la valeur minimale s'il en existe plusieurs.

209
Jon Skeet

REMARQUE: J'inclus cette réponse par souci d'exhaustivité, car le PO n'a pas indiqué la source de données et nous ne devrions pas émettre d'hypothèses.

Cette requête donne la réponse correcte, mais pourrait être plus lente car il pourrait être nécessaire de trier tout les éléments de People, selon la structure de données People qui est:

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

UPDATE: En fait, je ne devrais pas appeler cette solution "naïve", mais l'utilisateur doit savoir à quoi il demande. La "lenteur" de cette solution dépend des données sous-jacentes. S'il s'agit d'un tableau ou de List<T>, LINQ to Objects n'a pas d'autre choix que de trier la collection entière avant de sélectionner le premier élément. Dans ce cas, le processus 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 trier toutes les lignes. D'autres implémentations personnalisées de 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 parcourir la collection entière une fois. En fait, LINQ to Objects aurait pu (en théorie) créer des cas spéciaux dans OrderBy() pour des collections triées comme SortedList<T>, mais cela n’est pas le cas, à ma connaissance.

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

Ferait l'affaire

60
Rune FS

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

Je cherchais un moyen propre et efficace (temps (O)) de le faire. Et je pense en avoir 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 initiale:

Pour C # 7.0 et les versions supérieures prenant en charge valeur Tuple :

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

Pour les versions C # antérieures à 7.0, 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 valeurs de type Tuple et anonyme ont des comparateurs par défaut sensibles: 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.

Et comme le type anonyme et la valeur Tuple sont des types valeur, ils devraient être très efficaces.

NOTE

Dans mes implémentations ArgMin ci-dessus, j'ai supposé que DateOfBirth prenait le type DateTime pour plus de clarté et de simplicité. La question initiale demande d'exclure ces entrées avec le champ null DateOfBirth:

Les valeurs Null DateOfBirth sont définies sur DateTime.MaxValue afin de les exclure de la considération Min (en supposant qu'au moins une d'entre elles possède une DOB spécifiée).

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

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

La question de la mise en œuvre de ArgMin ou ArgMax est donc sans importance.

NOTE 2

L'approche ci-dessus comporte une mise en garde selon laquelle, lorsque deux instances ont la même valeur min, l'implémentation Min() essaiera de comparer les instances en tant que point de départ. Cependant, si la classe des instances n'implémente pas IComparable, une erreur d'exécution sera renvoyée:

Au moins un objet doit implémenter IComparable

Heureusement, cela peut encore être résolu assez proprement. L'idée est d'associer un "ID" de distanct à chaque entrée servant de lien décisif sans ambiguïté. Nous pouvons utiliser un identifiant incrémentiel pour chaque entrée. Toujours en prenant l'âge des personnes comme exemple:

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;
21
KFL

Solution sans paquets supplémentaires:

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

aussi vous pouvez l'envelopper dans l'extension:

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

et dans ce cas:

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

À propos ... O (n ^ 2) n'est pas la meilleure solution. Paul Betts a donné une solution plus grosse que la mienne. Mais ma solution reste LINQ et elle est plus simple et courte que d’autres solutions proposées ici.

11
Andrew
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();
}
3
JustDave

Ce qui suit est la solution 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é à des types dont les sélecteurs de propriétés pourraient renvoyer 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;
        });
    }
}

Tests:

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

Utilisation parfaitement simple d'agrégat (équivalent à se replier dans d'autres langues):

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

Le seul inconvénient est que la propriété est accédée deux fois par élément de séquence, ce qui peut être coûteux. C'est difficile à réparer.

0
david.pfx

EDIT à nouveau:

Pardon. En plus de rater nullable, je cherchais 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 consiste à implémenter IComparable et à utiliser Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>)) , qui renvoie en réalité un élément de la 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 en avez besoin, ou utiliser la mise en œuvre MoreLINQ donnée par Jon Skeet.

0
Matthew Flaschen