2009-07-25 20 views
7

Sau nhiều thử nghiệm tìm kiếm và mã của Google, tôi đã gặp phải vấn đề phức tạp về C# LINQ-to-objects trong SQL có thể dễ dàng giải quyết bằng một cặp hàm ROW_NUMBER() ... PARTITION BY và một truy vấn phụ hoặc hai.Chỉ mục LINQ-to-đối tượng trong một nhóm + cho các nhóm khác nhau (aka ROW_NUMBER với PARTITION BY tương đương)

Dưới đây là, trong lời nói, những gì tôi đang cố gắng để làm trong code-- yêu cầu cơ bản là loại bỏ các văn bản trùng lặp từ một danh sách:

  1. Đầu tiên, nhóm một danh sách bằng (Document.Title, tài liệu. SourceId), giả định định nghĩa lớp (đã đơn giản hóa) như sau:
     
    class Document 
    { 
        string Title; 
        int SourceId; // sources are prioritized (ID=1 better than ID=2) 
    }
  2. Trong nhóm đó, chỉ định mỗi tài liệu một chỉ mục (ví dụ: Chỉ mục 0 == Tài liệu thứ nhất với tiêu đề này từ nguồn này, Chỉ mục 1 = Tài liệu thứ 2 với tiêu đề từ nguồn này, v.v.) Tôi rất thích tương đương ROW_NUMBER() trong SQL!

  3. Bây giờ nhóm theo (Document.Title, Index), trong đó chỉ số được tính ở bướC# 2. Đối với mỗi nhóm, chỉ trả về một tài liệu: một tài liệu có Document.SourceId thấp nhất.

BướC# 1 rất dễ dàng (ví dụ codepronet.blogspot.com/2009/01/group-by-in-linq.html), nhưng tôi bị bối rối về các bướC# 2 và # 3. Tôi dường như không thể xây dựng một truy vấn C# LINQ màu đỏ-squiggle-miễn phí để giải quyết tất cả ba bước.

Bài đăng của Anders Heilsberg trên this thread là tôi nghĩ câu trả lời cho các bướC# 2 và # 3 ở trên nếu tôi có thể nhận được cú pháp đúng.

Tôi muốn tránh sử dụng biến cục bộ bên ngoài để thực hiện tính toán Chỉ mục, như được đề xuất trên slodge.blogspot.com/2009/01/adding-row-number-using-linq-to-objects.html, kể từ khi giải pháp đó phá vỡ nếu biến bên ngoài được sửa đổi. Tối ưu, bước theo từng nhóm có thể được thực hiện trước, vì vậy nhóm "bên trong" (đầu tiên theo Nguồn để tính toán chỉ mục, sau đó theo Chỉ mục để lọc ra các bản sao) có thể hoạt động trên một số lượng nhỏ các đối tượng trong mỗi nhóm "theo tiêu đề" nhóm, vì số lượng tài liệu trong mỗi nhóm tiêu đề thường dưới 100. Tôi thực sự không muốn một giải pháp N !

Tôi chắc chắn có thể giải quyết vấn đề này với vòng lặp forested lồng nhau, nhưng có vẻ như đó là loại sự cố đơn giản với LINQ.

Bất kỳ ý tưởng nào?

Trả lời

5

Tôi nghĩ jpbochi đã bỏ lỡ rằng bạn muốn nhóm của mình theo cặp giá trị (Tiêu đề + SourceId rồi Title + Index). Dưới đây là một truy vấn LINQ (chủ yếu) giải pháp:

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

Đầu tiên nhóm chúng tôi theo Tiêu đề + sourceid (tôi sử dụng một loại vô danh vì trình biên dịch xây dựng một hashcode tốt cho việc tra cứu nhóm). Sau đó, chúng tôi sử dụng Chọn để đính kèm chỉ mục được nhóm vào tài liệu mà chúng tôi sử dụng trong nhóm thứ hai của chúng tôi. Cuối cùng, đối với mỗi nhóm, chúng tôi chọn SourceId thấp nhất.

Với đầu vào này:

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

tôi nhận được kết quả này:

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

Cập nhật: Tôi chỉ thấy câu hỏi của bạn về nhóm theo Tiêu đề đầu tiên. Bạn có thể làm điều này bằng một subquery vào nhóm Tiêu đề của bạn:

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; 
+0

Hey DahlbyK - điều này thật tuyệt! Giải pháp của bạn có vẻ tốt. Bây giờ tôi không cảm thấy xấu về việc không thể tự mình tìm ra nó lần đầu tiên. Tôi phát hiện ra quá tải Select-with-index nhưng không thể tìm ra cách để đưa nó vào một truy vấn LINQ. Một số mã đai đen cuối cùng của bạn, nhờ sự giúp đỡ và giáo dục trong những gì có thể. –

3

Thành thật mà nói, tôi khá bối rối với câu hỏi của bạn. Có lẽ nếu bạn nên giải thích những gì bạn đang cố gắng giải quyết. Dù sao, tôi sẽ cố gắng trả lời những gì tôi hiểu.

1) Trước tiên, tôi giả định rằng bạn đã có danh sách tài liệu được nhóm theo Title + SourceId. Đối với mục đích thử nghiệm, tôi hardcoded một danh sách như sau:

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) Để có được đặt một chỉ mục trong mỗi mục, bạn có thể sử dụng phương pháp Select mở rộng, đi qua một hàm chọn Func. Như thế này:

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

3) Từ những gì tôi đã hiểu, bước tiếp theo sẽ là nhóm kết quả cuối cùng bằng Title.Dưới đây là cách thực hiện:

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

Hàm GroupBy (được sử dụng ở trên) trả về IEnumerable<IGrouping<string,DocumentWithIndex>>. Vì một nhóm cũng có thể đếm được, bây giờ chúng ta có một số đếm được.

4) Bây giờ, đối với mỗi nhóm ở trên, chúng tôi sẽ chỉ nhận được mặt hàng với mức tối thiểu SourceId. Để thực hiện thao tác này, chúng ta cần 2 mức đệ quy. Trong LINQ, mức bên ngoài là một lựa chọn (đối với từng nhóm, nhận được một trong các mục của nó), và mức độ bên trong là một tập hợp (được mục với mức thấp nhất SourceId):

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

Chỉ cần để đảm bảo rằng nó công trình, tôi đã thử nghiệm nó với một đơn giản 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 } 

tôi không chắc chắn đó là những gì bạn muốn. Nếu không, hãy bình luận câu trả lời và tôi có thể sửa câu trả lời. Tôi hi vọng cái này giúp được.

Obs .: Tất cả các lớp được sử dụng trong các bài kiểm tra của tôi là anonymous. Vì vậy, bạn không thực sự cần phải xác định loại DocumentWithIndex. Trên thực tế, tôi thậm chí không tuyên bố một lớp học Document.

+0

Hi jpochi - giải pháp dahlby là một điều đúng.xin lỗi tôi đã không thể lấy lại cho bạn sớm hơn để làm rõ, đây là câu hỏi đầu tiên của tôi trên tràn ngăn xếp và tôi không bao giờ mong đợi để có được 2 câu trả lời trong vòng chưa đầy 2 giờ vào một chủ nhật! Lần tới tôi sẽ kiểm tra lại nhanh hơn! :-) Dù sao, nhờ sự giúp đỡ. –

+0

Không vấn đề gì. Tôi đoán bạn nên đánh dấu câu trả lời của mình như được chấp nhận sau đó. – jpbochi

1

Phương pháp Dựa Cú pháp:

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

Bạn có nói ở trên là phương pháp tương đương cú pháp dựa?

+0

Đúng, điều này phát ra cùng một kết quả (chính xác) như cú pháp LINQ-y của DahlbyK ở trên. Mặc dù (xem truy vấn được cập nhật của Dahlby) có thể hiệu quả hơn khi nhóm theo Tiêu đề đầu tiên để phân loại/tổng hợp có thể xảy ra trên các bộ nhỏ - nếu có hàng tỷ tài liệu, nó sẽ tạo sự khác biệt lớn vì bạn không phải tải tất cả của chúng vào RAM cùng một lúc. Ngoài ra, hầu hết các tiêu đề sẽ không có bất kỳ bản sao nào cả ... Tôi hy vọng việc phân loại BCL được tối ưu hóa và nhóm theo các hoạt động trên một bộ thành viên. :-) –

1

Tôi đã triển khai phương pháp tiện ích. Nó hỗ trợ nhiều phân vùng theo lĩnh vực cũng như nhiều điều kiện đặt hàng.

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

Cách sử dụng:

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

Kết quả:

{ Title = "Title1", SourceId = 1 }, 
{ Title = "Title2", SourceId = 14 }, 
{ Title = "Title3", SourceId = 100 } 
Các vấn đề liên quan