2009-07-25 5 views
7

Después de mucha búsqueda de Google y experimentación de código, estoy perplejo sobre un problema complejo de C# LINQ-a-objetos que en SQL ser fácil de resolver con un par de ROW_NUMBER() ... PARTITION BY funciones y una subconsulta o dos.Índice de LINQ-a-objetos dentro de un grupo + para diferentes agrupaciones (también conocido como ROW_NUMBER con equivalente PARTITION BY)

Así es, en palabras, lo que trato de hacer en code-- el requisito fundamental es la eliminación de documentos duplicados de una lista:

  1. En primer lugar, el grupo de una lista por (Document.Title, documento. sourceid), asumiendo una definición (simplificado) clase como esta:
     
    class Document 
    { 
        string Title; 
        int SourceId; // sources are prioritized (ID=1 better than ID=2) 
    }
  2. Dentro de ese grupo, asignar a cada documento un índice (por ejemplo Índice 0 == primera documento con este título de esta fuente, Índice 1 = segundo documento con este título de esta fuente, etc.). Me encantaría el equivalente de ROW_NUMBER() en SQL.

  3. Ahora agrupe por (Document.Title, Índice), donde se calculó el Índice en el Paso # 2. Para cada grupo, devuelva solo un documento: el que tiene Document.SourceId más bajo.

Paso # 1 es fácil (por ejemplo codepronet.blogspot.com/2009/01/group-by-in-linq.html), pero yo estoy difuminados en los pasos # 2 y # 3. Parece que no puedo construir una consulta C# LINQ sin garabatos rojos para resolver los tres pasos.

La publicación de Anders Heilsberg en this thread es la respuesta a los pasos 2 y 3 anteriores si pudiera obtener la sintaxis correcta.

Preferiría evitar el uso de una variable local externa para hacer el cálculo del índice, como se recomienda en slodge.blogspot.com/2009/01/adding-row-number-using-linq-to-objects.html, ya que esa solución se rompe si la variable externa se modifica.

De manera óptima, el paso de agrupar por título podría hacerse primero, de modo que las agrupaciones "internas" (primero por origen para calcular el índice y luego por índice para filtrar duplicados) pueden operar en pequeños números de objetos en cada grupo "por título", ya que el número de documentos en cada grupo de título por lo general es menor de 100. ¡Realmente no quiero una solución N !

Sin duda podría resolver esto con bucles foreach anidados, pero parece ser el tipo de problema que debería ser simple con LINQ.

¿Alguna idea?

Respuesta

5

Creo que jpbochi olvidó que quiere que sus agrupaciones sean por pares de valores (Título + ID de fuente luego Título + Índice). He aquí una consulta LINQ (en su mayoría) solución:

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

primer grupo por el Título + sourceid (utilizo un tipo anónimo porque el compilador construye un buen código hash para la consulta de la agrupación). Luego usamos Seleccionar para adjuntar el índice agrupado al documento, que usamos en nuestra segunda agrupación. Finalmente, para cada grupo elegimos el SourceId más bajo.

Dada esta entrada:

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

puedo obtener este resultado:

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

Actualización: Acabo de ver tu pregunta sobre la agrupación por Título primero. Para ello, puede utilizar una subconsulta en sus grupos Título:

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

¡Hola DahlbyK - esto es genial! Tu solución se ve bien. Ahora no me siento tan mal por no ser capaz de resolverlo por mi cuenta la primera vez. Descubrí la sobrecarga Select-with-index pero no pude encontrar la forma de ingresarlo en una consulta LINQ. Algún código de cinturón negro en su extremo, gracias por la ayuda y la educación en lo que es posible. –

3

Para ser sincero, estoy bastante confundido con su pregunta. Tal vez si debe explicar lo que está tratando de resolver. De todos modos, trataré de responder lo que entendí.

1) Primero, supongo que ya tiene una lista de documentos agrupados por Title + SourceId. Para propósitos de prueba, que hardcoded una lista de la siguiente manera:

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) Para poner un índice en cada artículo, puede utilizar el método Select extensión, pasando una función del selector Func. De esta manera:

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

3) Por lo que entendí, el siguiente paso sería agrupar el último resultado por Title.Así es como se hace:

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

La función GroupBy (usado anteriormente) devuelve un IEnumerable<IGrouping<string,DocumentWithIndex>>. Como un grupo también es enumerable, ahora tenemos un enumerable de enumerables.

4) Ahora, para cada uno de los grupos anteriores, obtendremos solo el artículo con el mínimo SourceId. Para realizar esta operación, necesitaremos 2 niveles de recursión. En LINQ, el nivel exterior es una selección (para cada grupo, conseguir uno de sus elementos), y el nivel interior es una agregación (obtener el artículo con el más bajo SourceId):

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

sólo para asegurar que se trabajos, he comprobado con un simple 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 } 

no estoy seguro de que eso es lo que quería. Si no, por favor, comenten la respuesta y puedo arreglar la respuesta. Espero que esto ayude.

Obs .: Todas las clases utilizadas en mis pruebas fueron anonymous. Por lo tanto, realmente no necesita definir un tipo DocumentWithIndex. En realidad, ni siquiera he declarado una clase Document.

+0

Hola jpochi - La solución de Dahlby era una correcta.¡Lo siento, no pude contactarte antes para aclarar, esta fue mi primera pregunta sobre el desbordamiento de la pila y nunca esperé obtener 2 respuestas en menos de 2 horas en un domingo! ¡La próxima vez lo comprobaré más rápido! :-) De todos modos, gracias por la ayuda. –

+0

No hay problema. Supongo que deberías marcar su respuesta como aceptada entonces. – jpbochi

1

Método Sintaxis base:

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

diría que el anterior es la sintaxis basada Método equivalente?

+0

Sí, esto emite los mismos resultados (correctos) que la sintaxis de LINQ-y de DahlbyK anterior. Aunque (consulte la consulta actualizada de Dahlby) es probablemente más eficiente agrupar por título primero para que cualquier ordenamiento/agregación pueda ocurrir en conjuntos pequeños: si hubiera mil millones de documentos, haría una gran diferencia ya que no tendría que cargar todo de ellos en RAM a la vez. Además, la mayoría de los títulos no tendrán ningún duplicado ... Espero que el BCL haya optimizado la ordenación y las operaciones de agrupación por conjuntos de un miembro. :-) –

1

Implementé un método de extensión. Es compatible con múltiples particiones por campos, así como múltiples condiciones de pedido.

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

Uso:

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

Resultado:

{ Title = "Title1", SourceId = 1 }, 
{ Title = "Title2", SourceId = 14 }, 
{ Title = "Title3", SourceId = 100 } 
Cuestiones relacionadas