Verwendung von LINQ zur Auswahl eines Objekts mit minimalem oder maximalem Eigenschaftswert

466

Ich habe ein Personenobjekt mit einer Nullable DateOfBirth-Eigenschaft. Gibt es eine Möglichkeit, mit LINQ eine Liste von Personenobjekten für das Objekt mit dem frühesten / kleinsten DateOfBirth-Wert abzufragen?

Folgendes habe ich begonnen:

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

Null DateOfBirth-Werte werden auf DateTime.MaxValue gesetzt, um sie aus der Min-Betrachtung auszuschließen (vorausgesetzt, mindestens einer hat ein angegebenes DOB).

Für mich bedeutet das jedoch nur, firstBornDate auf einen DateTime-Wert zu setzen. Was ich erhalten möchte, ist das Person-Objekt, das dazu passt. Muss ich eine zweite Abfrage wie folgt schreiben:

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

Oder gibt es einen schlankeren Weg, dies zu tun?

Slolife
quelle
24
Nur ein Kommentar zu Ihrem Beispiel: Sie sollten Single hier wahrscheinlich nicht verwenden. Es würde eine Ausnahme auslösen, wenn zwei Personen das gleiche Geburtsdatum hätten
Niki
1
Siehe auch den fast doppelten stackoverflow.com/questions/2736236/… , der einige kurze Beispiele enthält.
Goodeye
4
Was für eine einfache und nützliche Funktion. MinBy sollte in der Standardbibliothek sein. Wir sollten eine Pull-Anfrage an Microsoft github.com/dotnet/corefx
Colonel Panic
2
Dies scheint heute zu existieren, bieten Sie nur eine Funktion, um die Eigenschaft a.Min(x => x.foo);
auszuwählen
4
Um das Problem zu demonstrieren: Gibt in Python max("find a word of maximal length in this sentence".split(), key=len)die Zeichenfolge 'Satz' zurück. In C # wird "find a word of maximal length in this sentence".Split().Max(word => word.Length)berechnet, dass 8 die längste Länge eines Wortes ist, aber nicht, was das längste Wort ist .
Colonel Panic

Antworten:

299
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
Ana Betts
quelle
16
Wahrscheinlich etwas langsamer als nur die Implementierung von IComparable und die Verwendung von Min (oder einer for-Schleife). Aber +1 für eine O (n) linqy-Lösung.
Matthew Flaschen
3
Außerdem muss es <curmin.DateOfBirth sein. Andernfalls vergleichen Sie eine DateTime mit einer Person.
Matthew Flaschen
2
Seien Sie auch vorsichtig, wenn Sie damit zwei Datumszeiten vergleichen. Ich habe dies verwendet, um den letzten Änderungsdatensatz in einer ungeordneten Sammlung zu finden. Es ist fehlgeschlagen, weil der von mir gewünschte Datensatz das gleiche Datum und die gleiche Uhrzeit hatte.
Simon Gill
8
Warum machst du den überflüssigen Check curMin == null? curMinkönnte nur sein, nullwenn Sie Aggregate()mit einem Samen verwenden, der ist null.
Gute Nacht Nerd Pride
226

Leider gibt es dafür keine integrierte Methode, aber es ist einfach genug, sie selbst zu implementieren. Hier sind die Eingeweide davon:

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

Anwendungsbeispiel:

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

Beachten Sie, dass dies eine Ausnahme auslöst, wenn die Sequenz leer ist, und das erste Element mit dem Minimalwert zurückgibt, wenn es mehr als ein Element gibt.

Alternativ können Sie die Implementierung verwenden, die wir in MoreLINQ in MinBy.cs haben . (Es gibt MaxBynatürlich eine entsprechende .)

Installation über die Paketmanager-Konsole:

PM> Install-Package morelinq

Jon Skeet
quelle
1
Ich würde den Ienumerator + währenddessen durch einen foreach ersetzen
ggf31416
5
Dies ist aufgrund des ersten Aufrufs von MoveNext () vor der Schleife nicht einfach. Es gibt Alternativen, aber sie sind unordentlicher IMO.
Jon Skeet
2
Während ich konnte zurückkehren default (T), die ich unpassend empfindet. Dies ist konsistenter mit Methoden wie First () und dem Ansatz des Dictionary-Indexers. Sie können es jedoch leicht anpassen, wenn Sie möchten.
Jon Skeet
8
Ich habe Paul die Antwort wegen der Nicht-Bibliothekslösung gegeben, aber danke für diesen Code und den Link zur MoreLINQ-Bibliothek, die ich wahrscheinlich verwenden werde!
Slolife
1
@ HamishGrubijan: ThrowHelper: code.google.com/p/morelinq/source/browse/MoreLinq/…
Jon Skeet
135

HINWEIS: Der Vollständigkeit halber füge ich diese Antwort hinzu, da das OP die Datenquelle nicht erwähnt hat und wir keine Annahmen treffen sollten.

Diese Abfrage gibt die richtige Antwort, kann jedoch langsamer sein, da je nach Datenstruktur möglicherweise alle Elemente sortiert werden müssen :PeoplePeople

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

UPDATE: Eigentlich sollte ich diese Lösung nicht "naiv" nennen, aber der Benutzer muss wissen, gegen was er abfragt. Die "Langsamkeit" dieser Lösung hängt von den zugrunde liegenden Daten ab. Wenn dies ein Array oder ist List<T>, hat LINQ to Objects keine andere Wahl, als zuerst die gesamte Sammlung zu sortieren, bevor das erste Element ausgewählt wird. In diesem Fall ist es langsamer als die andere vorgeschlagene Lösung. Wenn es sich jedoch um eine LINQ to SQL-Tabelle und DateOfBirtheine indizierte Spalte handelt, verwendet SQL Server den Index, anstatt alle Zeilen zu sortieren. Andere benutzerdefinierte IEnumerable<T>Implementierungen könnten ebenfalls Indizes verwenden (siehe i4o: Indexed LINQ oder die Objektdatenbank db4o ) und diese Lösung schneller als Aggregate()oder MaxBy()/ machenMinBy()die die gesamte Sammlung einmal iterieren müssen. Tatsächlich hätte LINQ to Objects (theoretisch) Sonderfälle OrderBy()für sortierte Sammlungen wie machen können SortedList<T>, aber meines Wissens nicht.

Lucas
quelle
1
Jemand hat das bereits gepostet, es aber anscheinend gelöscht, nachdem ich kommentiert hatte, wie langsam (und platzraubend) es war (O (n log n) Geschwindigkeit bestenfalls im Vergleich zu O (n) für min). :)
Matthew Flaschen
Ja, daher meine Warnung, die naive Lösung zu sein :) Es ist jedoch denkbar einfach und kann in einigen Fällen verwendet werden (kleine Sammlungen oder wenn DateOfBirth eine indizierte DB-Spalte ist)
Lucas
Ein weiterer Sonderfall (der auch nicht vorhanden ist) besteht darin, dass es möglich wäre, das Wissen von orderby zu nutzen und zunächst nach dem niedrigsten Wert zu suchen, ohne zu sortieren.
Rune FS
Das Sortieren einer Sammlung ist eine Nlog (N) -Operation, die nicht besser als die lineare oder O (n) -Zeitkomplexität ist. Wenn wir nur 1 Element / Objekt aus einer Sequenz benötigen, die min oder max ist, sollten wir uns an die lineare Zeitkomplexität halten.
Yawar Murtaza
@yawar die Sammlung könnte bereits sortiert sein (wahrscheinlicher indiziert). In diesem Fall können Sie O (log n)
Rune FS
63
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Würde den Trick machen

Rune FS
quelle
1
Dieser ist großartig! Ich habe mit OrderByDesending (...) verwendet. Nehmen Sie (1) in meinem Fall von Linq-Projektion.
Vedran Mandić
1
Dieser verwendet eine Sortierung, die die O (N) -Zeit überschreitet, und verwendet auch einen O (N) -Speicher.
George Polevoy
@ GeorgePolevoy, der davon ausgeht, dass wir ziemlich viel über die Datenquelle wissen. Wenn die Datenquelle bereits einen sortierten Index für das angegebene Feld hat, wäre dies eine (niedrige) Konstante und viel schneller als die akzeptierte Antwort, die erforderlich wäre, um die gesamte Liste zu durchlaufen. Wenn die Datenquelle andererseits zB ein Array ist, haben Sie natürlich Recht
Rune FS
@RuneFS - trotzdem solltest du das in deiner Antwort erwähnen, weil es wichtig ist.
rory.ap
Die Leistung wird Sie nach unten ziehen. Ich habe es auf die harte Tour gelernt. Wenn Sie das Objekt mit dem Min- oder Max-Wert haben möchten, müssen Sie nicht das gesamte Array sortieren. Nur 1 Scan sollte ausreichen. Schauen Sie sich die akzeptierte Antwort oder das MoreLinq-Paket an.
Sau001
35

Sie fragen also nach ArgMinoder ArgMax. C # hat keine integrierte API für diese.

Ich habe nach einem sauberen und effizienten (O (n) in der Zeit) Weg gesucht, um dies zu tun. Und ich glaube, ich habe einen gefunden:

Die allgemeine Form dieses Musters ist:

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

Insbesondere anhand des Beispiels in der ursprünglichen Frage:

Für C # 7.0 und höher unterstützt das Wertetupel :

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

Für die C # -Version vor 7.0 kann stattdessen ein anonymer Typ verwendet werden:

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

Sie funktionieren, weil sowohl Wertetupel als auch anonymer Typ sinnvolle Standardvergleiche haben: Für (x1, y1) und (x2, y2) werden zuerst x1vs x2und dann y1vs verglichen y2. Aus diesem Grund .Minkann das integrierte Gerät für diese Typen verwendet werden.

Und da sowohl anonymer Typ als auch Wertetupel Werttypen sind, sollten beide sehr effizient sein.

HINWEIS

In meinen obigen ArgMinImplementierungen habe ich aus Gründen der Einfachheit und Klarheit angenommen DateOfBirth, dass der Typ verwendet wird DateTime. In der ursprünglichen Frage werden die Einträge mit dem Nullfeld ausgeschlossen DateOfBirth:

Null DateOfBirth-Werte werden auf DateTime.MaxValue gesetzt, um sie aus der Min-Betrachtung auszuschließen (vorausgesetzt, mindestens einer hat ein angegebenes DOB).

Dies kann mit einer Vorfilterung erreicht werden

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

Es ist also unerheblich für die Frage der Implementierung ArgMinoder ArgMax.

ANMERKUNG 2

Der obige Ansatz hat die Einschränkung, dass die Min()Implementierung versucht, die Instanzen als Tie-Breaker zu vergleichen , wenn zwei Instanzen denselben Min-Wert haben . Wenn die Klasse der Instanzen jedoch nicht implementiert wird IComparable, wird ein Laufzeitfehler ausgegeben:

Mindestens ein Objekt muss IComparable implementieren

Zum Glück kann dies noch recht sauber behoben werden. Die Idee ist, jedem Eintrag, der als eindeutiger Krawattenbrecher dient, eine entfernte "ID" zuzuordnen. Wir können für jeden Eintrag eine inkrementelle ID verwenden. Verwenden Sie immer noch das Alter der Menschen als Beispiel:

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;
KFL
quelle
1
Dies scheint nicht zu funktionieren, wenn der Werttyp der Sortierschlüssel ist. "Mindestens ein Objekt muss IComparable implementieren"
liang
1
zu schön! Dies sollte die beste Antwort sein.
Guido Mocha
@liang ja guter Fang. Zum Glück gibt es dafür immer noch eine saubere Lösung. Siehe die aktualisierte Lösung in Abschnitt "Hinweis 2".
KFL
Select kann Ihnen die ID geben! var jüngste = Personen. Wählen Sie ((p, i) => (p.DateOfBirth, i, p)). Min (). Item2;
Jeremy
19

Lösung ohne zusätzliche Pakete:

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

Sie können es auch in eine Erweiterung einwickeln:

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

und in diesem Fall:

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

Übrigens ... O (n ^ 2) ist nicht die beste Lösung. Paul Betts gab Fatster-Lösung als meine. Aber meine Lösung ist immer noch LINQ und sie ist einfacher und kürzer als andere Lösungen hier.

Andrew
quelle
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
quelle
3

Perfekt einfache Verwendung des Aggregats (entspricht dem Falten in anderen Sprachen):

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

Der einzige Nachteil ist, dass zweimal pro Sequenzelement auf die Eigenschaft zugegriffen wird, was teuer sein kann. Das ist schwer zu beheben.

david.pfx
quelle
1

Das Folgende ist die allgemeinere Lösung. Es macht im Wesentlichen dasselbe (in O (N) Reihenfolge), jedoch bei allen IEnumberable-Typen und kann mit Typen gemischt werden, deren Eigenschaftsauswahl null zurückgeben könnte.

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
quelle
0

Nochmals BEARBEITEN:

Es tut uns leid. Abgesehen davon, dass ich das Nullable vermisst habe, habe ich mir die falsche Funktion angesehen.

Min <(Of <(TSource, TResult>)>) (IEnumerable <(Of <(TSource>)>), Func <(Of <(TSource, TResult>)>)) gibt den Ergebnistyp wie gesagt zurück.

Ich würde sagen, eine mögliche Lösung besteht darin, IComparable zu implementieren und Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>)) zu verwenden , das tatsächlich ein Element aus IEnumerable zurückgibt. Das hilft Ihnen natürlich nicht, wenn Sie das Element nicht ändern können. Ich finde das Design von MS hier etwas seltsam.

Natürlich können Sie bei Bedarf jederzeit eine for-Schleife ausführen oder die von Jon Skeet bereitgestellte MoreLINQ-Implementierung verwenden.

Matthew Flaschen
quelle
0

Eine andere Implementierung, die mit nullbaren Auswahlschlüsseln und für die Sammlung von Referenztypen arbeiten könnte, gibt null zurück, wenn keine geeigneten Elemente gefunden wurden. Dies kann beispielsweise bei der Verarbeitung von Datenbankergebnissen hilfreich sein.

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

Beispiel:

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);
Евгений Орлов
quelle
-2

Ich habe selbst nach etwas Ähnlichem gesucht, vorzugsweise ohne eine Bibliothek zu benutzen oder die gesamte Liste zu sortieren. Meine Lösung endete ähnlich wie die Frage selbst, nur ein wenig vereinfacht.

var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));
Sind
quelle
Wäre es nicht weitaus effizienter, die min vor Ihrer linq-Anweisung zu erhalten? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...Andernfalls wird die min wiederholt, bis die gesuchte gefunden wird.
Nieminen