7

多くの Google 検索とコードの実験の後、複雑な C# LINQ-to-objects 問題に困惑しました。SQL では、ROW_NUMBER()...PARTITION BY 関数のペアと 1 つまたは 2 つのサブクエリで簡単に解決できます。

つまり、私がコードでやろうとしていることは次のとおりです。根本的な要件は、重複するドキュメントをリストから削除することです。

  1. まず、(Document.Title, Document.SourceId) でリストをグループ化し、次のような (簡略化された) クラス定義を想定します。
    クラス ドキュメント
    {
        文字列のタイトル。
        int SourceId; // ソースが優先されます (ID=1 は ID=2 より優れています)
    }
  2. そのグループ内で、各ドキュメントにインデックスを割り当てます (たとえば、インデックス 0 == このソースからのこのタイトルの最初のドキュメント、インデックス 1 = このソースからのこのタイトルの 2 番目のドキュメントなど)。SQL の ROW_NUMBER() に相当するものが欲しいです!

  3. 今度は (Document.Title, Index) でグループ化します。Index は手順 2 で計算されました。グループごとに、Document.SourceId が最小のドキュメントを 1 つだけ返します。

ステップ 1 は簡単ですが (例: codepronet.blogspot.com/2009/01/group-by-in-linq.html)、ステップ 2 と 3 で困惑しています。3 つの手順すべてを解決するために、赤い波線のない C# LINQ クエリを作成することはできないようです。

このスレッドに関する Anders Heilsberg の投稿は、正しい構文を取得できれば、上記のステップ 2 と 3 に対する答えだと思います。

slodge.blogspot.com/2009/01/adding-row-number-using-linq-to-objects.html で推奨されているように、インデックスの計算を行うために外部ローカル変数を使用することは避けたいと思います。外部変数が変更された場合。

最適には、タイトルによるグループ化のステップを最初に実行できるため、「内部」のグループ化 (最初にソースでインデックスを計算し、次にインデックスで重複を除外する) で、各「タイトルで」少数のオブジェクトを操作できます。各タイトル グループ内のドキュメントの数は通常 100 未満であるため、N 2ソリューションは本当に必要ありません。

ネストされた foreach ループでこれを解決することはできますが、LINQ では単純な問題のように思えます。

何か案は?

4

4 に答える 4

6

jpbochi は、グループ化を値のペア (Title+SourceId から Title+Index) で行う必要があることを見逃したと思います。LINQクエリ(ほとんど)のソリューションは次のとおりです。

var selectedFew = 
    from doc in docs
    group doc by new { doc.Title, doc.SourceId } into g
    from docIndex in g.Select((d, i) => new { Doc = d, Index = i })
    group docIndex by new { docIndex.Doc.Title, docIndex.Index } into g
    select g.Aggregate((a,b) => (a.Doc.SourceId <= b.Doc.SourceId) ? a : b);

まず、Title+SourceId でグループ化します (コンパイラがグループ化ルックアップ用の適切なハッシュコードを作成するため、匿名型を使用します)。次に、Select を使用してグループ化されたインデックスをドキュメントに添付し、これを 2 番目のグループ化で使用します。最後に、グループごとに最も低い SourceId を選択します。

この入力を考えると:

var docs = new[] {
    new { Title = "ABC", SourceId = 0 },
    new { Title = "ABC", SourceId = 4 },
    new { Title = "ABC", SourceId = 2 },
    new { Title = "123", SourceId = 7 },
    new { Title = "123", SourceId = 7 },
    new { Title = "123", SourceId = 7 },
    new { Title = "123", SourceId = 5 },
    new { Title = "123", SourceId = 5 },
};

私はこの出力を得る:

{ Doc = { Title = ABC, SourceId = 0 }, Index = 0 }
{ Doc = { Title = 123, SourceId = 5 }, Index = 0 }
{ Doc = { Title = 123, SourceId = 5 }, Index = 1 }
{ Doc = { Title = 123, SourceId = 7 }, Index = 2 }

更新:タイトルによるグループ化に関する質問を最初に見ました。これは、タイトル グループのサブクエリを使用して行うことができます。

var selectedFew =
    from doc in docs
    group doc by doc.Title into titleGroup
    from docWithIndex in
        (
            from doc in titleGroup
            group doc by doc.SourceId into idGroup
            from docIndex in idGroup.Select((d, i) => new { Doc = d, Index = i })
            group docIndex by docIndex.Index into indexGroup
            select indexGroup.Aggregate((a,b) => (a.Doc.SourceId <= b.Doc.SourceId) ? a : b)
        )
    select docWithIndex;
于 2009-07-26T02:40:45.153 に答える
3

正直なところ、私はあなたの質問にかなり混乱しています。解決しようとしていることを説明する必要があるかもしれません。とにかく、私が理解したことに答えようとします。

Title1) まず、 +でグループ化されたドキュメントのリストが既にあると仮定しますSourceId。テスト目的で、次のようにリストをハードコーディングしました。

var docs = new [] {
    new { Title = "ABC", SourceId = 0 },
    new { Title = "ABC", SourceId = 4 },
    new { Title = "ABC", SourceId = 2 },
    new { Title = "123", SourceId = 7 },
    new { Title = "123", SourceId = 5 },
};

2) すべてのアイテムにインデックスを配置するには、Select拡張メソッドを使用して Func セレクター関数を渡します。このような:

var docsWithIndex
    = docs
    .Select( (d, i) => new { Doc = d, Index = i } );

3)私が理解したことから、次のステップは、最後の結果をTitle. 方法は次のとおりです。

var docsGroupedByTitle
    = docsWithIndex
    .GroupBy( a => a.Doc.Title );

GroupBy 関数 (上で使用) は を返しますIEnumerable<IGrouping<string,DocumentWithIndex>>。グループも列挙可能であるため、列挙型の列挙型ができました。

4) ここで、上記の各グループについて、最小値のアイテムのみを取得しますSourceId。この操作を行うには、2 レベルの再帰が必要です。LINQ では、外側のレベルは選択 (グループごとにその項目の 1 つを取得) であり、内側のレベルは集計 (最小の項目を取得SourceId) です。

var selectedFew
    = docsGroupedByTitle
    .Select(
        g => g.Aggregate(
            (a, b) => (a.Doc.SourceId  <= b.Doc.SourceId) ? a : b
        )
    );

それが機能することを確認するために、簡単な方法でテストしましたforeach

foreach (var a in selectedFew) Console.WriteLine(a);
//The result will be:
//{ Doc = { Title = ABC, SourceId = 0 }, Index = 0 }
//{ Doc = { Title = 123, SourceId = 5 }, Index = 4 }

それがあなたが望んでいたことかどうかはわかりません。そうでない場合は、答えをコメントしてください。答えを修正できます。これが役立つことを願っています。

Obs .: 私のテストで使用されたすべてのクラスはanonymousでした。したがって、型を定義する必要はありませんDocumentWithIndex。実際、私はDocumentクラスを宣言さえしていません。

于 2009-07-26T01:04:53.867 に答える
1

メソッドベースの構文:

var selectedFew = docs.GroupBy(doc => new {doc.Title, doc.SourceId}, doc => doc)
                      .SelectMany((grouping) => grouping.Select((doc, index) => new {doc, index}))
                              .GroupBy(anon => new {anon.doc.Title, anon.index})
                              .Select(grouping => grouping.Aggregate((a, b) =>    a.doc.SourceId <= b.doc.SourceId ? a : b));

上記は同等のメソッドベースの構文だと思いますか?

于 2009-07-27T23:04:24.750 に答える
1

拡張メソッドを実装しました。フィールドごとの複数のパーティションと複数の順序条件をサポートしています。

public static IEnumerable<TResult> Partition<TSource, TKey, TResult>(
    this IEnumerable<TSource> source, 
    Func<TSource, TKey> keySelector,
    Func<IEnumerable<TSource>, IOrderedEnumerable<TSource>> sorter,
    Func<TSource, int, TResult> selector)
{
    AssertUtilities.ArgumentNotNull(source, "source");

    return source
        .GroupBy(keySelector)
        .Select(arg => sorter(arg).Select(selector))
        .SelectMany(arg => arg);
}

使用法:

var documents = new[] 
{
    new { Title = "Title1", SourceId = 1 },
    new { Title = "Title1", SourceId = 2 },
    new { Title = "Title2", SourceId = 15 },
    new { Title = "Title2", SourceId = 14 },
    new { Title = "Title3", SourceId = 100 }
};

var result = documents
    .Partition(
        arg => arg.Title,  // partition by
        arg => arg.OrderBy(x => x.SourceId), // order by
        (arg, rowNumber) => new { RowNumber = rowNumber, Document = arg }) // select
    .Where(arg => arg.RowNumber == 0)
    .Select(arg => arg.Document)
    .ToList();

結果:

{ Title = "Title1", SourceId = 1 },
{ Title = "Title2", SourceId = 14 },
{ Title = "Title3", SourceId = 100 }
于 2014-03-26T02:05:02.110 に答える