2010-02-09 35 views
14

Tengo algunos datos que tienen varios atributos y quiero agruparlos jerárquicamente. Por ejemplo:¿Cómo puedo agrupar datos jerárquicamente utilizando LINQ?

public class Data 
{ 
    public string A { get; set; } 
    public string B { get; set; } 
    public string C { get; set; } 
} 

me gustaría que este agruparse como:

A1 
- B1 
    - C1 
    - C2 
    - C3 
    - ... 
- B2 
    - ... 
A2 
- B1 
    - ... 
... 

Actualmente, he podido grupo esto usando LINQ tal que el grupo superior divide los datos por A, entonces cada divide subgrupo por B, entonces cada subgrupo B contiene subgrupos de C, etc. El LINQ parece a esto (suponiendo una secuencia IEnumerable<Data> llamado data):

var hierarchicalGrouping = 
      from x in data 
      group x by x.A 
       into byA 
       let subgroupB = from x in byA 
           group x by x.B 
            into byB 
            let subgroupC = from x in byB 
                group x by x.C 
            select new 
            { 
             B = byB.Key, 
             SubgroupC = subgroupC 
            } 
       select new 
       { 
        A = byA.Key, 
        SubgroupB = subgroupB 
       }; 

Como puede ver, esto se vuelve un tanto desordenado cuanto más subgrupo se necesita. ¿Hay alguna manera más agradable de realizar este tipo de agrupación? Parece que debería haber y yo simplemente no lo estoy viendo.

actualización
Hasta ahora, he encontrado que la expresión de esta agrupación jerárquica mediante el uso de las API de LINQ con fluidez en lugar de lenguaje de consulta podría decirse que mejora la legibilidad, pero que no se siente muy seco.

Hubo dos formas en que hice esto: una usando GroupBy con un selector de resultados, la otra usando GroupBy seguido de una llamada Select. Ambos pueden formatearse para ser más legibles que el lenguaje de consulta, pero aún así no se escalan bien.

var withResultSelector = 
    data.GroupBy(a => a.A, (aKey, aData) => 
     new 
     { 
      A = aKey, 
      SubgroupB = aData.GroupBy(b => b.B, (bKey, bData) => 
       new 
       { 
        B = bKey, 
        SubgroupC = bData.GroupBy(c => c.C, (cKey, cData) => 
        new 
        { 
         C = cKey, 
         SubgroupD = cData.GroupBy(d => d.D) 
        }) 
       }) 
     }); 

var withSelectCall = 
    data.GroupBy(a => a.A) 
     .Select(aG => 
     new 
     { 
      A = aG.Key, 
      SubgroupB = aG 
       .GroupBy(b => b.B) 
       .Select(bG => 
      new 
      { 
       B = bG.Key, 
       SubgroupC = bG 
        .GroupBy(c => c.C) 
        .Select(cG => 
       new 
       { 
        C = cG.Key, 
        SubgroupD = cG.GroupBy(d => d.D) 
       }) 
      }) 
     }); 

Lo que me gustaría ...
puedo imaginar un par de maneras en que esto podría expresarse (suponiendo que el lenguaje y un marco apoyado). La primera sería una extensión GroupBy que toma una serie de pares de funciones para la selección de teclas y la selección de resultados, Func<TElement, TKey> y Func<TElement, TResult>. Cada par describe el siguiente subgrupo. Esta opción se cae porque cada par podría requerir que TKey y TResult sean diferentes a los demás, lo que significaría que GroupBy necesitarían parámetros finitos y una declaración compleja.

La segunda opción sería un método de extensión SubGroupBy que podría estar encadenado para producir subgrupos. SubGroupBy sería lo mismo que GroupBy pero el resultado sería la agrupación anterior más particionada. Por ejemplo:

var groupings = data 
    .GroupBy(x=>x.A) 
    .SubGroupBy(y=>y.B) 
    .SubGroupBy(z=>z.C) 

// This version has a custom result type that would be the grouping data. 
// The element data at each stage would be the custom data at this point 
// as the original data would be lost when projected to the results type. 
var groupingsWithCustomResultType = data 
    .GroupBy(a=>a.A, x=>new { ... }) 
    .SubGroupBy(b=>b.B, y=>new { ... }) 
    .SubGroupBy(c=>c.C, c=>new { ... }) 

La dificultad con esto es cómo poner en práctica los métodos más eficiente con mi comprensión actual, cada nivel sería volver a crear nuevos objetos con el fin de extender los objetos anteriores. La primera iteración crearía agrupaciones de A, la segunda crearía objetos que tienen una clave de A y agrupaciones de B, la tercera rehacería todo eso y agregaría las agrupaciones de C. Esto parece terriblemente ineficiente (aunque sospecho que mis opciones actuales) en realidad hacer esto de todos modos). Sería bueno que las llamadas pasaran alrededor de una meta-descripción de lo que se requería y las instancias solo se crearon en la última pasada, pero eso también suena difícil.Tenga en cuenta que el suyo es similar a lo que se puede hacer con GroupBy, pero sin las llamadas al método anidado.

Espero que todo eso tenga sentido. Supongo que estoy persiguiendo arcoiris aquí, pero quizás no.

Actualización - otra opción
Otra posibilidad que creo que es más elegante que mis sugerencias anteriores confía en que cada grupo de padres de ser sólo una llave y una secuencia de elementos secundarios (como en los ejemplos), al igual que IGrouping ofrece ahora. Eso significa que una opción para construir esta agrupación sería una serie de selectores de teclas y un solo selector de resultados.

Si las claves fueron limitadas a un tipo de conjunto, que no es razonable, entonces esto podría ser generado como una secuencia de selectores de llave y un selector de resultados, o un selector de resultados y una params de selectores de llave. Por supuesto, si las claves tenían que ser de diferentes tipos y niveles diferentes, esto se vuelve difícil de nuevo, excepto por una profundidad de jerarquía finita debido a la forma en que funciona la parametrización de los genéricos.

Éstos son algunos ejemplos ilustrativos de lo que quiero decir:

Por ejemplo:

public static /*<grouping type>*/ SubgroupBy(
    IEnumerable<Func<TElement, TKey>> keySelectors, 
    this IEnumerable<TElement> sequence, 
    Func<TElement, TResult> resultSelector) 
{ 
    ... 
} 

var hierarchy = data.SubgroupBy(
        new [] { 
         x => x.A, 
         y => y.B, 
         z => z.C }, 
        a => new { /*custom projection here for leaf items*/ }) 

O:

public static /*<grouping type>*/ SubgroupBy(
    this IEnumerable<TElement> sequence, 
    Func<TElement, TResult> resultSelector, 
    params Func<TElement, TKey>[] keySelectors) 
{ 
    ... 
} 

var hierarchy = data.SubgroupBy(
        a => new { /*custom projection here for leaf items*/ }, 
        x => x.A, 
        y => y.B, 
        z => z.C) 

Esto no resuelve las ineficiencias de implementación, pero debe resolver el complejo anidando Sin embargo, ¿cuál sería el tipo de devolución de esta agrupación? ¿Necesitaría mi propia interfaz o puedo usar IGrouping de alguna manera? ¿Cuánto debo definir o la profundidad variable de la jerarquía todavía lo hace imposible?

Mi conjetura es que este debe ser el mismo que el tipo de retorno de cualquier IGrouping llamada, pero ¿cómo el sistema de tipos inferir ese tipo si no participa en ninguno de los parámetros que se pasan?

Este problema me hace entender mucho más, lo cual es genial, pero me duele el cerebro.

+0

@ Jeff: ¿Podría publicar el tipo de código que le quiere * * para escribir (presumiblemente invocar una especie de ayudante) y entonces podemos ver lo que podemos hacer? Sospecho que es una de esas cosas que requerirá una sobrecarga diferente para cada nivel de jerarquía (por ejemplo, uno para 2 niveles, uno para 3, etc.) pero podría ser útil. –

+0

@jon skeet: seguro. Proporcionaré una actualización en breve. Siento que hay una solución más elegante pero no puedo verla. Hice un intento de especular mi llamada ayer, pero no cumple con las reglas de genéricos ya que cada uso de Func requiere diferentes tipos genéricos. –

+0

@Jon Skeet: Correcto, proporcioné algunos detalles sobre las opciones que he considerado (fuera del lenguaje o las restricciones del marco) y mi pensamiento general. –

Respuesta

8

Here is a description cómo puede implementar un mecanismo de agrupación jerárquica.

partir de esta descripción: Clase

Resultado: Método

public class GroupResult 
{ 
    public object Key { get; set; } 
    public int Count { get; set; } 
    public IEnumerable Items { get; set; } 
    public IEnumerable<GroupResult> SubGroups { get; set; } 
    public override string ToString() 
    { return string.Format("{0} ({1})", Key, Count); } 
} 

Extensión:

public static class MyEnumerableExtensions 
{ 
    public static IEnumerable<GroupResult> GroupByMany<TElement>(
     this IEnumerable<TElement> elements, 
     params Func<TElement, object>[] groupSelectors) 
    { 
     if (groupSelectors.Length > 0) 
     { 
      var selector = groupSelectors.First(); 

      //reduce the list recursively until zero 
      var nextSelectors = groupSelectors.Skip(1).ToArray(); 
      return 
       elements.GroupBy(selector).Select(
        g => new GroupResult 
        { 
         Key = g.Key, 
         Count = g.Count(), 
         Items = g, 
         SubGroups = g.GroupByMany(nextSelectors) 
        }); 
     } 
     else 
      return null; 
    } 
} 

Uso:

var result = customers.GroupByMany(c => c.Country, c => c.City); 

Editar:

Aquí es una versión mejorada y debidamente mecanografiado del código.

public class GroupResult<TItem> 
{ 
    public object Key { get; set; } 
    public int Count { get; set; } 
    public IEnumerable<TItem> Items { get; set; } 
    public IEnumerable<GroupResult<TItem>> SubGroups { get; set; } 
    public override string ToString() 
    { return string.Format("{0} ({1})", Key, Count); } 
} 

public static class MyEnumerableExtensions 
{ 
    public static IEnumerable<GroupResult<TElement>> GroupByMany<TElement>(
     this IEnumerable<TElement> elements, 
     params Func<TElement, object>[] groupSelectors) 
    { 
     if (groupSelectors.Length > 0) 
     { 
      var selector = groupSelectors.First(); 

      //reduce the list recursively until zero 
      var nextSelectors = groupSelectors.Skip(1).ToArray(); 
      return 
       elements.GroupBy(selector).Select(
        g => new GroupResult<TElement> { 
         Key = g.Key, 
         Count = g.Count(), 
         Items = g, 
         SubGroups = g.GroupByMany(nextSelectors) 
        }); 
     } else { 
      return null; 
     } 
    } 
} 
+0

Esto no me está creando en 'Items = g' ... y 'IEnumerable Items' si configuro 'IEnumerable Items' –

+0

@Prisoner ZERO: Los elementos son de TElement y no de GroupResult. Agregué una versión correctamente tipada a la publicación. – AxelEckenberger

+1

La versión mejorada no se compila. (GroupResult requiere 1 tipo de argumento.) –

4

Necesita una función recursiva. La función recursiva se llama a sí misma para cada nodo en el árbol.

Para hacer esto en Linq, puede use a Y-combinator.

+0

¿Cómo funcionaría cuando la propiedad que estoy agrupando cambie en cada nivel? –

+0

No es así. Es mejor que establezca una asociación autorreferencial añadiendo un ParentID a cada nodo (para que siempre se refiera a ParentID en cada nivel), a menos que, por supuesto, el número de niveles de árbol (profundidad anidada) esté limitado por su diseño de la aplicación. –

Cuestiones relacionadas