Cara menggunakan LINQ untuk memilih objek dengan nilai properti minimum atau maksimum

466

Saya memiliki objek Orang dengan properti DateOfBirth Nullable. Apakah ada cara untuk menggunakan LINQ untuk meminta daftar objek Orang untuk yang dengan nilai DateOfBirth paling awal / terkecil.

Inilah yang saya mulai dengan:

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

Nilai DateOfBirth kosong ditetapkan ke DateTime.MaxValue untuk mengesampingkan mereka dari pertimbangan Min (dengan asumsi setidaknya satu memiliki DOB yang ditentukan).

Tapi yang saya lakukan hanyalah mengatur firstBornDate menjadi nilai DateTime. Yang ingin saya dapatkan adalah objek Orang yang cocok dengan itu. Apakah saya perlu menulis permintaan kedua seperti:

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

Atau ada cara yang lebih ramping untuk melakukannya?

Slolife
sumber
24
Hanya komentar pada contoh Anda: Anda mungkin tidak boleh menggunakan Tunggal di sini. Ini akan membuang pengecualian jika dua Orang memiliki DateOfBirth yang sama
Niki
1
Lihat juga stackoverflow.com/questions/2736236/… yang hampir duplikat , yang memiliki beberapa contoh ringkas.
selamat tinggal
4
Sungguh fitur yang sederhana dan bermanfaat. MinBy seharusnya berada di perpustakaan standar. Kami harus mengirimkan permintaan tarik ke Microsoft github.com/dotnet/corefx
Kolonel Panic
2
Tampaknya ini tidak ada hari ini, cukup sediakan fungsi untuk memilih properti:a.Min(x => x.foo);
jackmott
4
Untuk menunjukkan masalah: dengan Python, max("find a word of maximal length in this sentence".split(), key=len)mengembalikan string 'kalimat'. Dalam C # "find a word of maximal length in this sentence".Split().Max(word => word.Length)menghitung bahwa 8 adalah panjang terpanjang kata apapun, tetapi tidak memberi tahu Anda apa kata terpanjang adalah .
Kolonel Panic

Jawaban:

299
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
Ana Betts
sumber
16
Mungkin sedikit lebih lambat daripada hanya mengimplementasikan IComparable dan menggunakan Min (atau a for loop). Tapi +1 untuk solusi O (n) linqy.
Matthew Flaschen
3
Juga, itu harus <curmin.DateOfBirth. Kalau tidak, Anda membandingkan DateTime dengan Seseorang.
Matthew Flaschen
2
Juga berhati-hatilah saat menggunakan ini untuk membandingkan dua kali tanggal. Saya menggunakan ini untuk menemukan catatan perubahan terakhir dalam koleksi yang tidak diurutkan. Gagal karena catatan yang saya inginkan berakhir dengan tanggal dan waktu yang sama.
Simon Gill
8
Mengapa Anda melakukan pemeriksaan berlebihan curMin == null? curMinhanya bisa nulljika Anda menggunakan Aggregate()dengan benih itu null.
Good Night Nerd Pride
226

Sayangnya tidak ada metode bawaan untuk melakukan ini, tetapi cukup mudah untuk diterapkan untuk Anda sendiri. Inilah keberaniannya:

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

Contoh penggunaan:

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

Perhatikan bahwa ini akan mengeluarkan pengecualian jika urutannya kosong, dan akan mengembalikan elemen pertama dengan nilai minimal jika ada lebih dari satu.

Atau, Anda dapat menggunakan implementasi yang kami miliki di MoreLINQ , di MinBy.cs . (Ada yang sesuai MaxBy, tentu saja.)

Instal melalui konsol manajer paket:

PM> Instal-Paket morelinq

Jon Skeet
sumber
1
Saya akan mengganti IEnumerator + sementara dengan foreach
ggf31416
5
Tidak bisa melakukan itu dengan mudah karena panggilan pertama ke MoveNext () sebelum loop. Ada alternatif, tetapi mereka IMO berantakan.
Jon Skeet
2
Sementara saya bisa mengembalikan default (T) yang terasa tidak pantas bagi saya. Ini lebih konsisten dengan metode seperti First () dan pendekatan pengindeks Kamus. Anda dapat dengan mudah menyesuaikannya jika Anda mau.
Jon Skeet
8
Saya memberikan jawaban kepada Paul karena solusi non-perpustakaan, tetapi terima kasih untuk kode ini dan tautan ke perpustakaan MoreLINQ, yang saya pikir saya akan mulai gunakan!
slolife
1
@HamishGrubijan: ThrowHelper: code.google.com/p/morelinq/source/browse/MoreLinq/…
Jon Skeet
135

CATATAN: Saya menyertakan jawaban ini untuk kelengkapan karena OP tidak menyebutkan apa sumber datanya dan kami tidak boleh membuat asumsi apa pun.

Kueri ini memberikan jawaban yang benar, tetapi bisa lebih lambat karena mungkin harus mengurutkan semua item People, tergantung pada struktur data apa People:

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

UPDATE: Sebenarnya saya tidak seharusnya menyebut solusi ini "naif", tetapi pengguna tidak perlu tahu apa yang ia tanyakan. "Kelambatan" solusi ini tergantung pada data yang mendasarinya. Jika ini adalah array atau List<T>, maka LINQ to Objects tidak punya pilihan selain mengurutkan seluruh koleksi terlebih dahulu sebelum memilih item pertama. Dalam hal ini akan lebih lambat daripada solusi lain yang disarankan. Namun, jika ini adalah tabel LINQ ke SQL dan DateOfBirthmerupakan kolom yang diindeks, maka SQL Server akan menggunakan indeks alih-alih menyortir semua baris. IEnumerable<T>Implementasi kustom lain juga dapat menggunakan indeks (lihat i4o: Indexed LINQ , atau database objek db4o ) dan menjadikan solusi ini lebih cepat daripadaAggregate() atau MaxBy()/MinBy()yang perlu mengulang seluruh koleksi sekali. Sebenarnya, LINQ to Objects bisa (secara teori) membuat case khusus OrderBy()untuk koleksi yang diurutkan seperti SortedList<T>, tapi tidak, sejauh yang saya tahu.

Lucas
sumber
1
Seseorang sudah memposting itu, tetapi ternyata menghapusnya setelah saya berkomentar seberapa lambat (dan memakan banyak ruang) kecepatannya (O (n log n) paling cepat dibandingkan dengan O (n) selama min). :)
Matthew Flaschen
ya, maka peringatan saya tentang menjadi solusi naif :) namun itu sederhana dan mungkin dapat digunakan dalam beberapa kasus (koleksi kecil atau jika DateOfBirth adalah kolom DB yang diindeks)
Lucas
kasus khusus lainnya (yang tidak ada di sana) adalah mungkin untuk menggunakan pengetahuan orderby dan pertama-tama mencari nilai terendah tanpa menyortir.
Rune FS
Mengurutkan koleksi adalah operasi Nlog (N) yang tidak lebih baik daripada kompleksitas waktu linear atau O (n). Jika kita hanya perlu 1 elemen / objek dari urutan yang min atau maks, saya pikir kita harus tetap dengan Linear time comlexity.
Yawar Murtaza
@yawar, koleksi mungkin sudah diurutkan (lebih mungkin diindeks) dalam hal ini Anda dapat memiliki O (log n)
Rune FS
63
People.OrderBy(p => p.DateOfBirth.GetValueOrDefault(DateTime.MaxValue)).First()

Akan melakukan triknya

Rune FS
sumber
1
Yang ini luar biasa! Saya menggunakan OrderByDesending (...). Ambil (1) dalam kasus saya projetion LINQ.
Vedran Mandić
1
Yang ini menggunakan pengurutan, yang melebihi waktu O (N) dan juga menggunakan memori O (N).
George Polevoy
@ GeorgePolevoy yang menganggap kita tahu cukup banyak tentang sumber data. Jika sumber data sudah memiliki indeks yang diurutkan pada bidang yang diberikan, maka ini akan menjadi konstanta (rendah) dan itu akan jauh lebih cepat daripada jawaban yang diterima yang akan perlu untuk menelusuri seluruh daftar. Jika sumber data di sisi lain adalah misalnya array Anda tentu saja benar
Rune FS
@ Runs - masih Anda harus menyebutkan itu dalam jawaban Anda karena itu penting.
rory.ap
Performa akan menyeret Anda ke bawah. Saya mempelajarinya dengan cara yang sulit. Jika Anda ingin objek dengan nilai Min atau Max, maka Anda tidak perlu mengurutkan seluruh array. Cukup 1 scan saja sudah cukup. Lihatlah jawaban yang diterima atau lihat paket MoreLinq.
Sau001
35

Jadi, Anda meminta ArgMinatau ArgMax. C # tidak memiliki API bawaan untuk itu.

Saya telah mencari cara yang bersih dan efisien (O (n) pada waktunya) untuk melakukan ini. Dan saya rasa saya menemukan satu:

Bentuk umum dari pola ini adalah:

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

Khususnya, menggunakan contoh dalam pertanyaan asli:

Untuk C # 7.0 ke atas yang mendukung nilai tuple :

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

Untuk versi C # sebelum 7.0, tipe anonim dapat digunakan sebagai gantinya:

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

Mereka bekerja karena kedua nilai tuple dan jenis anonim memiliki comparers standar yang masuk akal: untuk (x1, y1) dan (x2, y2), pertama kali membandingkan x1vs x2, kemudian y1vs y2. Itu sebabnya built-in .Mindapat digunakan pada tipe-tipe itu.

Dan karena tipe anonim dan nilai tuple adalah tipe nilai, keduanya harus sangat efisien.

CATATAN

Dalam ArgMinimplementasi saya di atas, saya berasumsi DateOfBirthuntuk mengetik DateTimeuntuk kesederhanaan dan kejelasan. Pertanyaan asli meminta untuk mengecualikan entri tersebut dengan DateOfBirthbidang nol :

Nilai DateOfBirth kosong ditetapkan ke DateTime.MaxValue untuk mengesampingkan mereka dari pertimbangan Min (dengan asumsi setidaknya satu memiliki DOB yang ditentukan).

Itu bisa dicapai dengan pre-filtering

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

Jadi tidak penting untuk pertanyaan implementasi ArgMinatauArgMax .

CATATAN 2

Pendekatan di atas memiliki peringatan bahwa ketika ada dua instance yang memiliki nilai min yang sama, maka Min()implementasi akan mencoba untuk membandingkan instance sebagai tie-breaker. Namun, jika kelas instance tidak mengimplementasikan IComparable, maka kesalahan runtime akan dilemparkan:

Setidaknya satu objek harus mengimplementasikan IComparable

Untungnya, ini masih bisa diperbaiki dengan agak bersih. Idenya adalah untuk mengasosiasikan "ID" jarak dengan setiap entri yang berfungsi sebagai tie-breaker yang jelas. Kita dapat menggunakan ID tambahan untuk setiap entri. Masih menggunakan usia orang sebagai contoh:

var youngest = Enumerable.Range(0, int.MaxValue)
               .Zip(people, (idx, ppl) => (ppl.DateOfBirth, idx, ppl)).Min().Item3;
KFL
sumber
1
Ini sepertinya tidak berfungsi ketika tipe nilai adalah kunci penyortiran. "Setidaknya satu objek harus mengimplementasikan IComparable"
liang
1
terlalu bagus! ini harus menjadi jawaban terbaik.
Guido Mocha
@liang ya tangkapan yang bagus. Untungnya masih ada solusi bersih untuk itu. Lihat solusi yang diperbarui di bagian "Catatan 2".
KFL
Pilih dapat memberi Anda ID! var termuda = people.Select ((p, i) => (p.DateOfBirth, i, p)). Min (). Item2;
Jeremy
19

Solusi tanpa paket tambahan:

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

Anda juga dapat membungkusnya menjadi ekstensi:

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

dan dalam hal ini:

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

Ngomong-ngomong ... O (n ^ 2) bukan solusi terbaik. Paul Betts memberikan solusi yang lebih gemuk daripada saya. Tapi solusi saya masih LINQ dan itu lebih sederhana dan lebih pendek daripada solusi lain di sini.

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

Penggunaan agregat yang sangat sederhana (setara dengan lipatan dalam bahasa lain):

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

Satu-satunya downside adalah bahwa properti diakses dua kali per elemen urutan, yang mungkin mahal. Itu sulit diperbaiki.

david.pfx
sumber
1

Berikut ini adalah solusi yang lebih umum. Ini pada dasarnya melakukan hal yang sama (dalam urutan O (N)) tetapi pada setiap jenis IEnumberable dan dapat dicampur dengan jenis yang selektor propertinya dapat mengembalikan nol.

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

Tes:

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

Sunting lagi:

Maaf. Selain kehilangan nullable saya melihat fungsi yang salah,

Min <(Dari <(TSource, TResult>)>) (IEnumerable <(Dari <(TSource>)>), Func <(Dari <(TSource, TResult>)>)) mengembalikan tipe hasil seperti yang Anda katakan.

Saya akan mengatakan salah satu solusi yang mungkin adalah dengan mengimplementasikan IComparable dan menggunakan Min <(Of <(TSource>)>) (IEnumerable <(Of <(TSource>)>))) , yang benar-benar mengembalikan elemen dari IEnumerable. Tentu saja, itu tidak membantu Anda jika Anda tidak dapat memodifikasi elemen. Saya menemukan desain MS agak aneh di sini.

Tentu saja, Anda selalu dapat melakukan for for loop jika perlu, atau menggunakan implementasi MoreLINQ yang diberikan Jon Skeet.

Matthew Flaschen
sumber
0

Implementasi lain, yang dapat bekerja dengan kunci pemilih yang dapat dibatalkan, dan untuk koleksi jenis referensi mengembalikan nol jika tidak ada elemen yang cocok ditemukan. Ini bisa membantu kemudian memproses hasil basis data misalnya.

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

Contoh:

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

Saya sendiri sedang mencari sesuatu yang serupa, lebih disukai tanpa menggunakan perpustakaan atau mengurutkan seluruh daftar. Solusi saya akhirnya mirip dengan pertanyaan itu sendiri, hanya sedikit disederhanakan.

var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == People.Min(p2 => p2.DateOfBirth));
Adalah
sumber
Bukankah jauh lebih efisien untuk mendapatkan min sebelum pernyataan LINQ Anda? var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...Kalau tidak, itu mendapatkan min berulang kali sampai menemukan yang Anda cari.
Nieminen