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

466

J'ai un objet Person avec une propriété Nullable DateOfBirth. Existe-t-il un moyen d'utiliser LINQ pour interroger une liste d'objets Person 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 a un DOB spécifié).

Mais tout ce que cela fait pour moi, c'est de définir firstBornDate sur une valeur DateTime. Ce que j'aimerais obtenir, c'est l'objet Personne qui correspond à cela. Dois-je écrire une deuxième requête comme ceci:

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

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

slolife
la source
24
Juste un commentaire sur votre exemple: vous ne devriez probablement pas utiliser Single ici. Cela lèverait une exception si deux personnes avaient le même DateOfBirth
Niki
1
Voir également le stackoverflow.com/questions/2736236/… presque dupliqué , qui contient des exemples concis.
goodeye
4
Quelle fonctionnalité simple et utile. MinBy devrait être dans la bibliothèque standard. Nous devons soumettre une demande d'extraction à Microsoft github.com/dotnet/corefx
Colonel Panic du
2
Cela semble exister aujourd'hui, il suffit de fournir une fonction pour choisir la propriété:a.Min(x => x.foo);
jackmott
4
Pour illustrer le problème: en Python, max("find a word of maximal length in this sentence".split(), key=len)retourne la chaîne 'phrase'. En C # "find a word of maximal length in this sentence".Split().Max(word => word.Length)calcule que 8 est la plus longue d'un mot, mais ne vous dit pas ce que le mot le plus long est .
Colonel Panic

Réponses:

299
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
Ana Betts
la source
16
Probablement un peu plus lent que d'implémenter IComparable et d'utiliser Min (ou une boucle for). Mais +1 pour une solution O (n) linqy.
Matthew Flaschen
3
En outre, il doit être <curmin.DateOfBirth. Sinon, vous comparez un DateTime à une personne.
Matthew Flaschen
2
Soyez également prudent lorsque vous utilisez ceci pour comparer deux heures de date. J'utilisais cela pour trouver le dernier enregistrement de changement dans une collection non ordonnée. Il a échoué parce que le disque que je voulais se retrouvait avec la même date et heure.
Simon Gill
8
Pourquoi faites-vous le contrôle superflu curMin == null? curMinne pourrait l'être que nullsi vous l'utilisiez Aggregate()avec une graine qui l'est null.
Good Night Nerd Pride
226

Malheureusement, il n'y a pas de méthode intégrée pour le faire, mais c'est assez facile à mettre en œuvre par vous-même. En voici les tripes:

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

Exemple d'utilisation:

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

Notez que cela lèvera une exception si la séquence est vide et retournera le premier élément avec la valeur minimale s'il y en a plus d'un.

Alternativement, vous pouvez utiliser l'implémentation que nous avons dans MoreLINQ , dans MinBy.cs . (Il y a un correspondant MaxBy, bien sûr.)

Installer via la console du gestionnaire de packages:

PM> Install-Package morelinq

Jon Skeet
la source
1
Je remplacerais l'Ienumerator + par un foreach
ggf31416
5
Impossible de le faire facilement en raison du premier appel à MoveNext () avant la boucle. Il existe des alternatives, mais elles sont plus désordonnées à l'OMI.
Jon Skeet
2
Bien que je puisse retourner par défaut (T), cela me semble inapproprié. Ceci est plus cohérent avec des méthodes comme First () et l'approche de l'indexeur de dictionnaire. Vous pouvez facilement l'adapter si vous le souhaitez.
Jon Skeet
8
J'ai attribué la réponse à Paul à cause de la solution sans bibliothèque, mais merci pour ce code et ce lien vers la bibliothèque MoreLINQ, que je pense que je vais commencer à utiliser!
slolife
1
@HamishGrubijan: ThrowHelper: code.google.com/p/morelinq/source/browse/MoreLinq/…
Jon Skeet
135

REMARQUE: J'inclus cette réponse pour être complet car le PO n'a pas mentionné la source des données et nous ne devons faire aucune hypothèse.

Cette requête donne la bonne réponse, mais peut être plus lente car elle peut devoir trier tous les éléments People, selon la structure des données 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 a besoin de 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 List<T>, alors 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, il sera plus lent que l'autre solution suggérée. Toutefois, s'il s'agit d'une table LINQ to SQL et d' DateOfBirthune colonne indexée, SQL Server utilisera l'index au lieu de trier toutes les lignes. D'autres IEnumerable<T>implémentations personnalisées pourraient également utiliser des index (voir i4o: LINQ indexé ou la base de données d'objets db4o ) et rendre cette solution plus rapide que Aggregate()ou MaxBy()/MinBy()qui ont besoin d'itérer la collection entière une fois. En fait, LINQ to Objects aurait pu (en théorie) créer des cas spéciaux OrderBy()pour des collections triées comme SortedList<T>, mais ce n'est pas le cas, à ma connaissance.

Lucas
la source
1
Quelqu'un a déjà posté cela, mais l'a apparemment supprimé après avoir commenté la lenteur (et la consommation d'espace) (vitesse O (n log n) au mieux par rapport à O (n) pendant min). :)
Matthew Flaschen
oui, d'où mon avertissement d'être la solution naïve :) cependant c'est simple et peut être utilisable dans certains cas (petites collections ou si DateOfBirth est une colonne DB indexée)
Lucas
un autre cas particulier (qui n'est pas là non plus) est qu'il serait possible d'utiliser la connaissance de orderby et d'abord de rechercher la valeur la plus basse sans trier.
Rune FS
Le tri d'une collection est une opération Nlog (N) qui n'est pas meilleure que la complexité temporelle linéaire ou O (n). Si nous avons juste besoin d'un élément / objet d'une séquence qui est min ou max, je pense que nous devrions nous en tenir à la complexité temporelle linéaire.
Yawar Murtaza
@yawar la collection est peut-être déjà triée (indexée plus probablement), auquel cas vous pouvez avoir O (log n)
Rune FS
63
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Ferait l'affaire

Rune FS
la source
1
Celui-ci est super! J'ai utilisé avec OrderByDesending (...). Prenez (1) dans mon cas de linq projetion.
Vedran Mandić
1
Celui-ci utilise le tri, qui dépasse le temps O (N) et utilise également la mémoire O (N).
George Polevoy
@GeorgePolevoy qui suppose que nous en savons beaucoup sur la source de données. Si la source de données a déjà un index trié sur le champ donné, alors ce serait une constante (faible) et ce serait beaucoup plus rapide que la réponse acceptée qui nécessiterait de parcourir la liste entière. Si la source de données d'autre part est par exemple un tableau, vous avez bien sûr raison
Rune FS
@RuneFS - vous devez quand même le mentionner dans votre réponse car c'est important.
rory.ap
La performance vous entraînera vers le bas. Je l'ai appris à la dure. Si vous souhaitez que l'objet ait la valeur Min ou Max, vous n'avez pas besoin de trier le tableau entier. Un seul scan devrait suffire. Regardez la réponse acceptée ou regardez le paquet MoreLinq.
Sau001
35

Vous demandez donc ArgMinou ArgMax. C # n'a pas d'API intégrée pour ceux-ci.

Je cherchais un moyen 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(.)

Surtout, en utilisant l'exemple de la question d'origine:

Pour C # 7.0 et supérieur qui 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 travaillent parce que les deux tuple de valeur et le type anonyme ont par défaut sensées comparateurs: pour (x1, y1) et (x2, y2), il compare d' abord x1vs x2, puis y1vs y2. C'est pourquoi le intégré .Minpeut être utilisé sur ces types.

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

REMARQUE

Dans mes ArgMinimplémentations ci-dessus, j'ai supposé DateOfBirthprendre le type DateTimepour plus de simplicité et de clarté. La question d'origine demande d'exclure ces entrées avec un DateOfBirthchamp 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 une a un DOB spécifié).

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

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

C'est donc sans importance pour la question de la mise en œuvre de ArgMinou ArgMax.

NOTE 2

L'approche ci-dessus a une mise en garde: lorsque deux instances ont la même valeur minimale, l' Min()implémentation essaiera de comparer les instances en tant que bris d'égalité. Cependant, si la classe des instances n'est pas implémentée IComparable, une erreur d'exécution sera levée:

Au moins un objet doit implémenter IComparable

Heureusement, cela peut encore être résolu de manière assez nette. L'idée est d'associer un "ID" distanct à chaque entrée qui sert de bris d'égalité sans ambiguïté. Nous pouvons utiliser un ID incrémentiel pour chaque entrée. En utilisant toujours l'âge des gens comme exemple:

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;
KFL
la source
1
Cela ne semble pas fonctionner lorsque le type de valeur est la clé de tri. "Au moins un objet doit implémenter IComparable"
liang
1
trop bien! ce devrait être la meilleure réponse.
Guido Mocha
@liang oui bonne capture. Heureusement, il existe encore une solution propre à cela. Voir la solution mise à jour dans la section "Note 2".
KFL
Sélectionnez peut vous donner l'ID! var youngest = people.Select ((p, i) => (p.DateOfBirth, i, p)). Min (). Item2;
Jeremy
19

Solution sans packages supplémentaires:

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

vous pouvez également 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);

Au fait ... O (n ^ 2) n'est pas la meilleure solution. Paul Betts a donné une solution plus grosse que la mienne. Mais ma solution est toujours LINQ et elle est plus simple et plus courte que les autres solutions ici.

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

Utilisation parfaitement simple de l'agrégat (équivalent à se plier 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 accessible deux fois par élément de séquence, ce qui peut être coûteux. C'est difficile à résoudre.

david.pfx
la source
1

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

Tests:

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

MODIFIER à nouveau:

Désolé. En plus de manquer la valeur nulle, 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 retourne 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 for si vous en avez besoin, ou utiliser l'implémentation MoreLINQ de Jon Skeet.

Matthew Flaschen
la source
0

Une autre implémentation, qui pourrait fonctionner avec des clés de sélection nullables, et pour la collection de type de référence, renvoie null si aucun élément approprié n'a été trouvé. Cela pourrait être utile pour le traitement des résultats de la base de données par exemple.

  public static class IEnumerableExtensions
  {
    /// <summary>
    /// Returns the element with the maximum value of a selector function.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of source.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
    /// <param name="source">An IEnumerable collection values to determine the element with the maximum value of.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
    /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
    /// <returns>The element in source with the maximum value of a selector function.</returns>
    public static TSource MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, 1);

    /// <summary>
    /// Returns the element with the minimum value of a selector function.
    /// </summary>
    /// <typeparam name="TSource">The type of the elements of source.</typeparam>
    /// <typeparam name="TKey">The type of the key returned by keySelector.</typeparam>
    /// <param name="source">An IEnumerable collection values to determine the element with the minimum value of.</param>
    /// <param name="keySelector">A function to extract the key for each element.</param>
    /// <exception cref="System.ArgumentNullException">source or keySelector is null.</exception>
    /// <exception cref="System.InvalidOperationException">source contains no elements.</exception>
    /// <returns>The element in source with the minimum value of a selector function.</returns>
    public static TSource MinBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) => MaxOrMinBy(source, keySelector, -1);


    private static TSource MaxOrMinBy<TSource, TKey>
      (IEnumerable<TSource> source, Func<TSource, TKey> keySelector, int sign)
    {
      if (source == null) throw new ArgumentNullException(nameof(source));
      if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
      Comparer<TKey> comparer = Comparer<TKey>.Default;
      TKey value = default(TKey);
      TSource result = default(TSource);

      bool hasValue = false;

      foreach (TSource element in source)
      {
        TKey x = keySelector(element);
        if (x != null)
        {
          if (!hasValue)
          {
            value = x;
            result = element;
            hasValue = true;
          }
          else if (sign * comparer.Compare(x, value) > 0)
          {
            value = x;
            result = element;
          }
        }
      }

      if ((result != null) && !hasValue)
        throw new InvalidOperationException("The source sequence is empty");

      return result;
    }
  }

Exemple:

public class A
{
  public int? a;
  public A(int? a) { this.a = a; }
}

var b = a.MinBy(x => x.a);
var c = a.MaxBy(x => x.a);
Евгений Орлов
la source
0

Essayez l'idée suivante:

var firstBornDate = People.GroupBy(p => p.DateOfBirth).Min(g => g.Key).FirstOrDefault();
ncnylon
la source
-2

Je cherchais quelque chose de similaire moi-même, de préférence sans utiliser de bibliothèque ou trier la liste entière. Ma solution a fini par ressembler à la question elle-même, juste un peu simplifiée.

var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));
Sont
la source
Ne serait-il pas beaucoup plus efficace d'obtenir le min avant votre instruction linq? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...Sinon, il obtient le min à plusieurs reprises jusqu'à ce qu'il trouve celui que vous recherchez.
Nieminen