web-dev-qa-db-ja.com

LINQを使用して最小または最大性能値を持つオブジェクトを選択する方法

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

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

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

それとももっと簡単な方法はありますか?

409
slolife
People.Aggregate((curMin, x) => (curMin == null || (x.DateOfBirth ?? DateTime.MaxValue) <
    curMin.DateOfBirth ? x : curMin))
269
Paul Betts

残念ながら、これを行うための組み込みの方法はありません。

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

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

あるいは、 MoreLINQMinBy.cs にある実装を使用することもできます。 (もちろん対応するMaxByがあります。)これがその中身です:

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

シーケンスが空の場合、これは例外をスローし、複数ある場合は最小値を持つfirst要素を返します。

205
Jon Skeet

注:OPはデータソースが何であるかについて言及していないため、完全性のためにこの回答を含めます。

このクエリでは正しい答えが得られますが、より遅くなる可能性があります並べ替える必要がある場合があるためallデータ構造Peopleの内容に応じて、Peopleのアイテムを並べ替えます。

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

更新:実際、このソリューションを「ナイーブ」と呼ぶべきではありませんが、ユーザーはクエリの対象を知る必要があります。このソリューションの「遅さ」は、基礎となるデータに依存します。これが配列またはList<T>の場合、LINQ to Objectsには、最初のアイテムを選択する前にコレクション全体を最初にソートする以外に選択肢はありません。この場合、他の解決策が提案するよりも遅くなります。ただし、これがLINQ to SQLテーブルであり、DateOfBirthがインデックス付き列である場合、SQL Serverはすべての行を並べ替える代わりにインデックスを使用します。他のカスタムIEnumerable<T>実装もインデックスを使用し( i4o:Indexed LINQ 、またはオブジェクトデータベース db4o を参照)、このソリューションをAggregate()またはMaxBy()より高速にします。/MinBy()は、コレクション全体を1回繰り返す必要があります。実際、(理論的には)LINQ to ObjectsはSortedList<T>のようなソートされたコレクションに対してOrderBy()で特別なケースを作成できましたが、私が知る限りはそうではありません。

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

トリックをしますか

60
Rune FS

だからあなたはArgMinArgMaxを求めています。 C#はそれらのための組み込みAPIを持っていません。

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

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

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

特に、元の質問の例を使用してください。

値Tuple をサポートする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;

値Tupleと匿名型の両方が実用的なデフォルト比較子を持っているのでうまくいきます。(x1、y1)と(x2、y2)の場合、まずx1x2を比較し、次にy1y2を比較します。そのため、組み込みの.Minをこれらの型で使用できるのはこのためです。

そして匿名型と値Tupleは両方とも値型なので、両方とも非常に効率的であるべきです。

NOTE

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

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

それは事前フィルタリングで達成することができます

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

そのため、ArgMinまたはArgMaxを実装するという問題は重要ではありません。

注2

上記の方法では、同じmin値を持つインスタンスが2つある場合、Min()実装はそれらのインスタンスをタイブレーカーとして比較しようとします。ただし、インスタンスのクラスがIComparableを実装していない場合は、ランタイムエラーが発生します。

少なくとも1つのオブジェクトはIComparableを実装しなければなりません

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

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

追加パッケージなしの解決策:

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)は最善の解決策ではありません。 Paul Bettsは、私よりも優れた解決策を示しました。しかし、私はまだLINQソリューションであり、それはここで他のソリューションよりもシンプルで短いです。

9
Andrew
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
JustDave

以下はより一般的な解決策です。本質的には同じことを(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
Zafar Ameem

集合体の完全に単純な使用法(他の言語ではfoldに相当):

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

唯一の欠点は、プロパティがsequence要素ごとに2回アクセスされることです。これは、コストがかかる可能性があります。それを修正するのは難しいです。

0
david.pfx

もう一度編集します。

ごめんなさい。 null許容値を見逃したことに加えて、間違った関数を見ていました。

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

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

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

0