Jak używać LINQ do wyboru obiektu o minimalnej lub maksymalnej wartości właściwości

465

Mam obiekt Person z właściwością Nullable DateOfBirth. Czy istnieje sposób użycia LINQ do przeszukania listy obiektów Person dla obiektu o najwcześniejszej / najmniejszej wartości DateOfBirth.

Oto, od czego zacząłem:

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

Wartości Null DateOfBirth są ustawiane na DateTime.MaxValue, aby wykluczyć je z uwzględnienia wartości minimalnej (zakładając, że co najmniej jedna ma określoną DOB).

Ale dla mnie wszystkim jest ustawienie firstBornDate na wartość DateTime. Chciałbym uzyskać obiekt Person, który do niego pasuje. Czy muszę napisać drugie zapytanie w ten sposób:

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

A może istnieje prostszy sposób na zrobienie tego?

slolife
źródło
24
Tylko komentarz do twojego przykładu: Prawdopodobnie nie powinieneś tutaj używać Single. Rzuciłoby to wyjątek, gdyby dwie osoby miały ten sam DateOfBirth
Niki
1
Zobacz także prawie zduplikowany stackoverflow.com/questions/2736236/... , który zawiera kilka zwięzłych przykładów.
goodeye,
4
Co za prosta i przydatna funkcja. MinBy powinien znajdować się w standardowej bibliotece. Powinniśmy przesłać żądanie ściągnięcia do Microsoft github.com/dotnet/corefx
pułkownik Panic
2
Wygląda na to, że istnieje dzisiaj, wystarczy zapewnić funkcję wyboru właściwości:a.Min(x => x.foo);
jackmott
4
Aby zademonstrować problem: w Pythonie max("find a word of maximal length in this sentence".split(), key=len)zwraca ciąg „zdanie”. W języku C # "find a word of maximal length in this sentence".Split().Max(word => word.Length)oblicza, że 8 jest najdłuższa długość każdego słowa, ale nie powiem ci, co najdłuższa słowo jest .
Pułkownik Panic

Odpowiedzi:

298
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
Ana Betts
źródło
16
Prawdopodobnie trochę wolniej niż implementacja IComparable i użycie Min (lub pętli for). Ale +1 za rozwiązanie O (n) linqy.
Matthew Flaschen
3
Ponadto musi to być <curmin.DateOfBirth. W przeciwnym razie porównujesz DateTime z osobą.
Matthew Flaschen
2
Należy również zachować ostrożność, używając tego do porównywania dwóch dat i godzin. Użyłem tego, aby znaleźć rekord ostatniej zmiany w nieuporządkowanej kolekcji. Nie udało się, ponieważ płyta, której szukałem, zakończyła się tą samą datą i godziną.
Simon Gill,
8
Dlaczego przeprowadzasz zbędną kontrolę curMin == null? curMinmoże być tylko nullwtedy, gdy używasz Aggregate()z ziarnem, które jest null.
Good Night Nerd Pride
226

Niestety nie ma wbudowanej metody, ale jest to dość łatwe do wdrożenia dla siebie. Oto jego zalety:

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

Przykładowe użycie:

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

Zauważ, że spowoduje to zgłoszenie wyjątku, jeśli sekwencja jest pusta, i zwróci pierwszy element o minimalnej wartości, jeśli jest więcej niż jeden.

Alternatywnie możesz użyć implementacji, którą mamy w MoreLINQ , w MinBy.cs . (Oczywiście istnieje odpowiednik MaxBy).

Zainstaluj za pomocą konsoli menedżera pakietów:

PM> Zainstaluj pakiet morelinq

Jon Skeet
źródło
1
Chciałbym wymienić Ienumerator + na foreach
ggf31416
5
Nie można tego łatwo zrobić z powodu pierwszego wywołania MoveNext () przed pętlą. Istnieją alternatywy, ale są bardziej chaotyczne IMO.
Jon Skeet
2
Chociaż ja mógł wrócić domyślną (T), która czuje się nieodpowiednie do mnie. Jest to bardziej spójne z metodami takimi jak First () i podejściem indeksatora Dictionary. Możesz go łatwo dostosować, jeśli chcesz.
Jon Skeet
8
Odpowiedzi udzieliłem Paulowi z powodu rozwiązania pozabibliotecznego, ale dziękuję za ten kod i link do biblioteki MoreLINQ, którą, jak sądzę, zacznę używać!
slolife
1
@HamishGrubijan: ThrowHelper: code.google.com/p/morelinq/source/browse/MoreLinq/…
Jon Skeet
135

UWAGA: Załączam tę odpowiedź dla kompletności, ponieważ PO nie wspomniał o tym, jakie jest źródło danych i nie powinniśmy przyjmować żadnych założeń.

To zapytanie daje poprawną odpowiedź, ale może być wolniejsze, ponieważ może być konieczne sortowanie wszystkich elementów, w Peoplezależności od struktury danych People:

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

AKTUALIZACJA: Właściwie nie powinienem nazywać tego rozwiązania „naiwnym”, ale użytkownik musi wiedzieć, o co pyta. „Powolność” tego rozwiązania zależy od podstawowych danych. Jeśli jest to tablica lub List<T>, wówczas LINQ do obiektów nie ma innego wyboru, jak najpierw posortować całą kolekcję przed wybraniem pierwszego elementu. W takim przypadku będzie on wolniejszy niż inne sugerowane rozwiązanie. Jeśli jednak jest to tabela LINQ do SQL i DateOfBirthkolumna indeksowana, wówczas SQL Server użyje indeksu zamiast sortować wszystkie wiersze. Inne niestandardowe IEnumerable<T>implementacje mogą również korzystać z indeksów (patrz i4o: Indeksowany LINQ lub obiektowa baza danych db4o ) i sprawić, że to rozwiązanie będzie szybsze niż Aggregate()lubMaxBy() /MinBy()które muszą raz iterować całą kolekcję. W rzeczywistości LINQ do Objects mógł (teoretycznie) zrobić specjalne przypadki OrderBy()dla posortowanych kolekcji takich jak SortedList<T>, ale o ile mi wiadomo, nie robi tego.

Lucas
źródło
1
Ktoś już to opublikował, ale najwyraźniej usunął go po tym, jak skomentowałem, jak wolne (i zajmujące dużo miejsca) było (najlepiej O (n log n) w porównaniu do O (n) przez min). :)
Matthew Flaschen
tak, stąd moje ostrzeżenie o byciu naiwnym rozwiązaniem :) jednak jest to bardzo proste i może być przydatne w niektórych przypadkach (małe kolekcje lub jeśli DateOfBirth jest indeksowaną kolumną DB)
Lucas
innym szczególnym przypadkiem (którego również nie ma) jest to, że można by skorzystać z wiedzy orderby i najpierw szukać najniższej wartości bez sortowania.
Run FS
Sortowanie kolekcji jest operacją Nlog (N), która nie jest lepsza niż złożoność liniowa lub czasowa O (n). Jeśli potrzebujemy tylko 1 elementu / obiektu z sekwencji, która jest minimalna lub maksymalna, myślę, że powinniśmy trzymać się liniowej złożoności czasu.
Yawar Murtaza
@yawar kolekcja może być już posortowana (bardziej zindeksowana), w którym to przypadku możesz mieć O (log n)
Rune FS
63
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Zrobiłby lewę

Rune FS
źródło
1
Ten jest świetny! Użyłem z OrderByDesending (...). Weź (1) w moim przypadku projekcji linq.
Vedran Mandić
1
Ten wykorzystuje sortowanie, które przekracza czas O (N), a także wykorzystuje pamięć O (N).
George Polevoy
@GeorgePolevoy, który zakłada, że ​​wiemy dość dużo o źródle danych. Jeśli źródło danych ma już posortowany indeks w danym polu, byłaby to (niska) stała i byłaby znacznie szybsza niż zaakceptowana odpowiedź, która wymagałaby przejścia całej listy. Jeśli natomiast źródłem danych jest np. Tablica, to oczywiście masz rację
Rune FS
@RuneFS - nadal powinieneś wspomnieć o tym w swojej odpowiedzi, ponieważ jest to ważne.
rory.ap
Spektakl przyciągnie cię w dół. Nauczyłem się tego na własnej skórze. Jeśli chcesz obiekt o wartości Min lub Max, nie musisz sortować całej tablicy. Wystarczy 1 skan. Spójrz na zaakceptowaną odpowiedź lub spójrz na pakiet MoreLinq.
Sau001
35

Więc prosisz o ArgMinlub ArgMax. C # nie ma dla nich wbudowanego API.

Szukałem czystego i wydajnego (O (n) na czas) sposobu, aby to zrobić. I myślę, że znalazłem jeden:

Ogólna forma tego wzoru to:

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

Szczególnie, korzystając z przykładu z oryginalnego pytania:

W wersji C # 7.0 i nowszej obsługującej krotkę wartości :

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

W wersji C # wcześniejszej niż 7.0 można użyć zamiast tego typu anonimowego :

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

Pracują ponieważ zarówno krotka wartość i typ anonimowy mieć sensowne domyślne porównywarki: dla (x1, y1) i (x2, y2), najpierw porównuje x1vs x2, następnie y1vs y2. Dlatego wbudowanego .Minmożna używać na tych typach.

A ponieważ zarówno anonimowy typ, jak i krotka wartości są typami wartości, oba powinny być bardzo wydajne.

UWAGA

W moich powyższych ArgMinimplementacjach przyjąłem DateOfBirthtyp DateTimedla uproszczenia i przejrzystości. Pierwotne pytanie dotyczy wykluczenia tych wpisów o pustym DateOfBirthpolu:

Wartości Null DateOfBirth są ustawiane na DateTime.MaxValue, aby wykluczyć je z uwzględnienia wartości minimalnej (zakładając, że co najmniej jedna ma określoną DOB).

Można to osiągnąć dzięki filtrowaniu wstępnemu

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

Jest to więc nieistotne dla kwestii wdrożenia ArgMinlub ArgMax.

UWAGA 2

Powyższe podejście ma zastrzeżenie, że jeśli istnieją dwa wystąpienia, które mają tę samą wartość minimalną, wówczas Min()implementacja spróbuje porównać wystąpienia jako rozstrzygające. Jeśli jednak klasa instancji nie zostanie zaimplementowana IComparable, zostanie wygenerowany błąd środowiska wykonawczego:

Co najmniej jeden obiekt musi implementować IComparable

Na szczęście można to nadal naprawić dość czysto. Chodzi o to, aby skojarzyć odrębny „identyfikator” z każdym wpisem, który służy jako jednoznaczny element rozstrzygający. Możemy użyć przyrostowego identyfikatora dla każdego wpisu. Nadal wykorzystując wiek osób jako przykład:

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;
KFL
źródło
1
Nie działa to, gdy typ wartości jest kluczem sortującym. „Co najmniej jeden obiekt musi implementować IComparable”
liang
1
za dobrze! to powinna być najlepsza odpowiedź.
Guido Mocha
@liang yes good catch. Na szczęście wciąż istnieje na to czyste rozwiązanie. Zobacz zaktualizowane rozwiązanie w sekcji „Uwaga 2”.
KFL
Wybierz może dać ci identyfikator! var youngest = people.Select ((p, i) => (p.DateOfBirth, i, p)). Min (). Item2;
Jeremy,
19

Rozwiązanie bez dodatkowych pakietów:

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

możesz także owinąć go w rozszerzenie:

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

iw tym przypadku:

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

Nawiasem mówiąc ... O (n ^ 2) nie jest najlepszym rozwiązaniem. Paul Betts dał tłustsze rozwiązanie niż moje. Ale moje wciąż jest rozwiązaniem LINQ i jest prostsze i krótsze niż inne rozwiązania tutaj.

Andrzej
źródło
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
źródło
3

Idealnie proste użycie agregatu (odpowiednik fold w innych językach):

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

Jedynym minusem jest to, że dostęp do właściwości uzyskuje się dwa razy na element sekwencji, co może być kosztowne. Trudno to naprawić.

david.pfx
źródło
1

Poniżej przedstawiono bardziej ogólne rozwiązanie. Zasadniczo robi to samo (w kolejności O (N)), ale na dowolnych typach IEnumerable i może mieszać się z typami, których selektory właściwości mogą zwracać 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;
        });
    }
}

Testy:

var nullableInts = new int?[] {5, null, 1, 4, 0, 3, null, 1};
Assert.AreEqual(0, nullableInts.MinBy(i => i));//should pass
zafar
źródło
0

EDYTUJ ponownie:

Przepraszam. Poza brakiem wartości zerowej patrzyłem na niewłaściwą funkcję,

Min <(Of <(TSource, TResult>)>) (IEnumerable <(Of <(TSource>)>), Func <(Of <(TSource, TResult>)>)) zwraca typ wyniku, jak powiedziałeś.

Powiedziałbym, że jednym z możliwych rozwiązań jest wdrożenie IComparable i użycie Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>)) , co tak naprawdę zwraca element z IEnumerable. Oczywiście to nie pomoże, jeśli nie możesz zmodyfikować elementu. Uważam, że projekt MS jest trochę dziwny.

Oczywiście zawsze możesz zrobić pętlę for, jeśli potrzebujesz, lub użyć implementacji MoreLINQ, którą dał Jon Skeet.

Matthew Flaschen
źródło
0

Inna implementacja, która może działać z zerowanymi kluczami selektora i dla kolekcji typu odwołania zwraca null, jeśli nie znaleziono odpowiednich elementów. Może to być pomocne na przykład w przypadku przetwarzania wyników bazy danych.

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

Przykład:

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);
Евгений Орлов
źródło
-2

Sam szukałem czegoś podobnego, najlepiej bez korzystania z biblioteki lub sortowania całej listy. Moje rozwiązanie skończyło się podobnie jak samo pytanie, tylko trochę uproszczone.

var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));

źródło
Czy nie byłoby o wiele bardziej wydajne uzyskanie min przed wyciągiem linq? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...W przeciwnym razie dostajemy minę wielokrotnie, aż znajdzie tę, której szukasz.
Nieminen