LINQを使用して最小または最大のプロパティ値を持つオブジェクトを選択する方法


466

Nullable DateOfBirthプロパティを持つPersonオブジェクトがあります。LINQを使用して、Personオブジェクトのリストをクエリし、DateOfBirthの値が最も早い/最も小さいものを探します。

これが私が始めたものです:

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

Null DateOfBirth値は、Minの考慮事項から除外するためにDateTime.MaxValueに設定されます(少なくとも1つに指定されたDOBがあると想定)。

しかし、私にとっては、firstBornDateをDateTime値に設定するだけです。取得したいのは、それに一致するPersonオブジェクトです。次のような2番目のクエリを作成する必要がありますか?

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

それとも、より無駄のない方法がありますか?


24
あなたの例についてのコメント:ここではおそらくSingleを使用すべきではありません。2人が同じDateOfBirthを持っている場合は例外がスローされます
Niki

1
ほとんど重複しているstackoverflow.com/questions/2736236/…も参照してください。簡潔な例がいくつかあります。
goodeye 2013

4
なんてシンプルで便利な機能でしょう。MinByは標準ライブラリにあるべきです。Microsoft github.com/dotnet/corefxに
大佐パニック

2
これは、単にプロパティを選択する機能を提供し、今日存在するように見えるん:a.Min(x => x.foo);
jackmott

4
問題を示すために:Pythonではmax("find a word of maximal length in this sentence".split(), key=len)、文字列 'sentence'を返します。C#では"find a word of maximal length in this sentence".Split().Max(word => word.Length)、8はどの単語の最長でもあると計算されますが、最長の単語何であるかはわかりません。
大佐パニック

回答:


299
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))

16
おそらく、IComparableを実装してMin(またはforループ)を使用するよりも少し遅いでしょう。しかし、O(n)linqyソリューションの場合は+1。
Matthew Flaschen、2009年

3
また、<curmin.DateOfBirthである必要があります。それ以外の場合は、DateTimeをPersonと比較しています。
Matthew Flaschen、2009年

2
これを使用して2つの日時を比較する場合にも注意してください。これを使用して、順序付けされていないコレクションの最後の変更レコードを見つけていました。欲しいレコードが同じ日時になってしまったので失敗しました。
Simon Gill

8
なぜ余計なチェックをするのcurMin == null?であるシードで使用している場合にcurMinのみ可能です。nullAggregate()null
おやすみオタクプライド


226

残念ながら、これを行う組み込みの方法はありませんが、自分で実装するのは簡単です。ここにその要点があります:

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

使用例:

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

シーケンスが空の場合は例外がスローされ、複数ある場合は最小値を持つ最初の要素が返されることに注意してください。

また、あなたは私たちが持っているの実装を使用することができMoreLINQで、MinBy.csを。(MaxByもちろん、対応するがあります。)

パッケージマネージャーコンソールからインストール:

PM>インストールパッケージmorelinq


1
Ienumerator +をforeachに置き換えます
ggf31416 2009年

5
ループの前のMoveNext()への最初の呼び出しのため、これを簡単に行うことはできません。代替案はありますが、それらはより面倒なIMOです。
ジョンスキート、

2
私はしばらくの間でした私には不適切な感じデフォルト(T)を返します。これは、First()のようなメソッドおよびディクショナリインデクサーのアプローチとより一貫しています。ただし、必要に応じて簡単に変更できます。
ジョンスキート、

8
ライブラリではない解決策のおかげで私はPaulに回答を与えましたが、このコードとMoreLINQライブラリへのリンクに感謝します。
slolife 2009年


135

注:OPはデータソースとは何も言及しておらず、想定もしないので、完全を期すためにこの回答を含めています。

このクエリは正しい答えを与えますが、データ構造によっては、のすべてのアイテムをソートする必要がある場合があるため、遅くなる可能性があります。PeoplePeople

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

更新:実際には私はこのソリューションを「素朴」と呼ぶべきではありませんが、ユーザーは自分が何に対してクエリを実行しているかを知る必要があります。このソリューションの「遅さ」は、基礎となるデータに依存します。これが配列またはのList<T>場合、LINQ to Objectsは、最初のアイテムを選択する前にコレクション全体を最初にソートする以外に選択肢はありません。この場合、推奨される他のソリューションよりも遅くなります。ただし、これがLINQ to SQLテーブルでDateOfBirthあり、インデックス付きの列である場合、SQL Serverはすべての行を並べ替えるのではなく、インデックスを使用します。その他のカスタムIEnumerable<T>実装はまた、索引の使用を作ることができる(参照:インデックスLINQ i4oを、またはオブジェクトデータベースdb4oのをし、より速く、この溶液を作製)、Aggregate()またはMaxBy()/MinBy()コレクション全体を一度繰り返す必要があります。実際、LINQ to Objectsは、(理論的には)のOrderBy()ような並べ替えられたコレクションに対して特別なケースを作った可能性がありますがSortedList<T>、私が知る限りそうではありません。


1
誰かがすでに投稿しましたが、それがどれほど遅い(そしてスペースを消費する)か(O(n log n)の速度は最低でもO(n)と比較して)とコメントした後、明らかに削除しました。:)
Matthew Flaschen、2009年

はい、したがって、素朴な解決策についての私の警告:)しかし、それは完全に単純であり、場合によっては使用できる可能性があります(小さなコレクションまたはDateOfBirthがインデックス付きDB列である場合)
Lucas

別の特殊なケース(これもありません)は、orderbyの知識を使用して、最初にソートせずに最低値を検索することが可能であることです。
Rune FS

コレクションの並べ替えは、線形またはO(n)時間の複雑さよりも優れているNlog(N)操作です。最小値または最大値であるシーケンスから1つの要素/オブジェクトのみが必要な場合、線形時間の複雑さを維持する必要があると思います。
Yawar Murtaza、

@yawarコレクションがすでにソートされ(可能性が高いインデックス付け)可能性がある場合には、あなたはO(ログn)を持つことができます
ルーンFS

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

トリックを行います


1
これは素晴らしい!私はOrderByDesending(...)を使用しました。私のlinq射影の場合はTake(1)を使用しました。
VedranMandić2015

1
これは、O(N)時間を超える並べ替えを使用し、O(N)メモリも使用します。
George Polevoy、

@GeorgePolevoyは、データソースについてかなりの知識があることを前提としています。データソースが指定されたフィールドに並べ替え済みのインデックスを既に持っている場合、これは(低い)定数であり、リスト全体をトラバースする必要がある承認された回答よりもはるかに高速になります。一方、データソースがアレイである場合はもちろん問題ありません
Rune FS

@RuneFS-それは重要なので、それでも回答でそれを言及する必要があります。
rory.ap 2018

パフォーマンスはあなたを引きずります。私はそれを難し​​い方法で学びました。最小値または最大値を持つオブジェクトが必要な場合は、配列全体を並べ替える必要はありません。1回のスキャンで十分です。受け入れられた回答を見るか、MoreLinqパッケージを見てください。
Sau001

35

ですから、ArgMinまたはを求めていますArgMax。C#にはそれらの組み込みAPIがありません。

私はこれを行うためのクリーンで効率的な(適時にO(n))方法を探していました。そして私はそれを見つけたと思います:

このパターンの一般的な形式は次のとおりです。

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

特に、元の質問の例を使用すると:

値のタプルをサポートするC#7.0以降の場合

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

7.0より前のC#バージョンの場合は、代わりに匿名型を使用できます。

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

(x1、y1)と(x2、y2)とするために、それは最初の比較:値タプルと匿名型の両方が、賢明なデフォルトする比較器を持っているので、彼らは仕事x1x2し、y1y2。そのため、.Minこれらのタイプで組み込みを使用できます。

また、匿名型と値タプルはどちらも値型であるため、どちらも非常に効率的です。

注意

上記のArgMin実装では、単純さと明快さのためにDateOfBirth型を取ると仮定しましたDateTime。元の質問では、DateOfBirthフィールドがnullのエントリを除外するよう求められます。

Null DateOfBirth値は、Minの考慮事項から除外するためにDateTime.MaxValueに設定されます(少なくとも1つに指定されたDOBがあると想定)。

事前フィルタリングで実現できます

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

したがって、ArgMinまたはの実装の問題には重要ではありませんArgMax

注2

上記のアプローチには、同じ最小値を持つ2つのインスタンスがある場合、Min()実装がタイブレーカーとしてインスタンスを比較しようとするという警告があります。ただし、インスタンスのクラスがを実装していない場合はIComparable、実行時エラーがスローされます。

少なくとも1つのオブジェクトはIComparableを実装する必要があります

幸いにも、これはまだかなりきれいに修正することができます。このアイデアは、明確な「ID」を明確なタイブレーカーとして機能する各エントリに関連付けることです。エントリごとに増分IDを使用できます。例としてまだ人々の年齢を使用しています:

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

1
これは、値のタイプがソートキーの場合は機能しないようです。「少なくとも1つのオブジェクトはIComparableを実装する必要があります」
liang

1
すごすぎる!これが最良の答えです。
グイドモカ

@liangはい良いキャッチ。幸いなことに、それに対する明確な解決策はまだあります。セクション2の更新されたソリューションを参照してください。
KFL

選択するとIDが付与されます。var youngest = people.Select((p、i)=>(p.DateOfBirth、i、p))。Min()。Item2;
ジェレミー

19

追加パッケージなしのソリューション:

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

また、あなたはそれを拡張にラップすることができます:

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

この場合:

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

ちなみに... O(n ^ 2)は最善の解決策ではありません。 ポール・ベッツは私よりも太った解決策を与えました。しかし、私はまだLINQソリューションであり、ここでは他のソリューションよりもシンプルで短いです。


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

3

集約の完全に単純な使用(他の言語のfoldに相当):

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

唯一の欠点は、プロパティがシーケンス要素ごとに2回アクセスされることです。それを修正するのは難しいです。


1

以下は、より一般的なソリューションです。これは基本的に同じことを(O(N)順で)行いますが、IEnumberable型に対して行われ、プロパティセレクターが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;
        });
    }
}

テスト:

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

0

もう一度編集:

ごめんなさい。nullableを逃した以外に、私は間違った関数を見ていた、

Min <(Of <(TSource、TResult>)>)(IEnumerable <(Of <(TSource>)>)、Func <(Of <(TSource、TResult>)>))は、あなたが言ったように結果の型を返します。

IComparableを実装し、Min <(Of <(TSource>)>)(IEnumerable <(Of <(TSource>)>))を使用するのが1つの可能な解決策だと思います。これは、実際にIEnumerableから要素を返します。もちろん、要素を変更できない場合は役に立ちません。MSのデザインはここで少し変だと思います。

もちろん、必要に応じて常にforループを実行したり、Jon Skeetが提供したMoreLINQ実装を使用したりできます。


0

null可能なセレクターキーを使用して、適切な要素が見つからない場合は参照型のコレクションに対してnullを返す別の実装。これは、たとえばデータベースの結果を処理する場合に役立ちます。

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

例:

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

-2

ライブラリを使用したり、リスト全体を並べ替えたりせずに、自分と似たものを探していました。私の解決策は、質問自体と同様になり、少し単純化されました。

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

linqステートメントの前に分を取得する方がはるかに効率的ではないでしょうか?var min = People.Min(...); var firstBorn = People.FirstOrDefault(p => p.DateOfBirth == min...それ以外の場合は、探しているものが見つかるまで繰り返し最小値を取得します。
Nieminen
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.