martes, 24 de marzo de 2015

La claúsula GroupBy




Continuamos avanzando dentro de los operadores de consulta de LinQ, y ahora le toca el turno a Group By. Group By es otro de los operadores de consulta de proyección, que a grandes rasgos, nos permite desde una colección de entrada, devolver una colección de salida agrupada, mediante un sencillo grupo de claves valor. Donde la clave estará compuesta por los campos por los que hemos agrupados (los que forman el group by) y el valor lo formarán una colección de elementos que comparten los mismos valores para las propiedades que forman la clave.







Recuerda que aquí tienes el indice de todos los posts del Curso de LinQ.


Hasta ahora, prácticamente todos nuestros métodos extensores de LinQ, devolvían una ‘Colección’ (un objeto del tipo de datos de IEnumerable<TElement>), el operador Group By, por su definición devolverá un conjunto de objetos de un tipo de datos un poco más avanzado IGrouping<out TKey, out TElement>. Esta interfaz implementa también IEnumerable<TElement> , conteniendo a su vez la propiedad Key que representa el atributo común a todos los elementos de los objetos de su grupo.

Algo a señalar de la interfaz  IGrouping<out TKey, out TElement> es que sus parámetros de tipo son out, por lo que pueden disfrutar de las bondades de la covarianza y contravarianza que también hemos visto con anterioridad y que forma parte de los delegados genéricos.


IEnumerable<IGrouping<TKey, Tsource>> será el tipo de datos de resultado de la clausula Group By.



Ejemplo de agrupación por un campo simple

Vamos a comenzar con un ejemplo de agrupación sencillo, por un único campo. En este ejemplo la Key la formará una única propiedad de nuestro objeto (TElement), generando grupos que pertenezcan a esta propiedad:




Este ejemplo tan sencillo lo trasladaríamos así al código:


class Automovil
{
    public string           Marca       { get; set; }
    public string           Modelo      { get; set; }
    public TipoCarburante   Carburante  { get; set; }
    public Color            Color       { get; set; }
}
 
 
public enum TipoCarburante
{
    Gasolina,
    Diesel
}


Apuntar que la clase Color del campo del mismo nombre, está en el ensamblado System.Windows.Media.


Vamos a ver el código de agrupación:


static void Main(string[] args)
{
 
    var coches = new List<Automovil>
    {
        new Automovil { Marca = "Renault", Modelo = "Clio"  , Carburante = TipoCarburante.Gasolina, Color = Colors.Green },
        new Automovil { Marca = "Citroen", Modelo = "C3"    , Carburante = TipoCarburante.Diesel  , Color = Colors.Gray  },
        new Automovil { Marca = "Renault", Modelo = "Laguna", Carburante = TipoCarburante.Diesel  , Color = Colors.Black },
        new Automovil { Marca = "Renault", Modelo = "Megane", Carburante = TipoCarburante.Gasolina, Color = Colors.Blue  },
        new Automovil { Marca = "Citroen", Modelo = "C4"    , Carburante = TipoCarburante.Gasolina, Color = Colors.Black }
    };
 
    /// Lambda
    //var resultado = coches.GroupBy((c => c.Marca).ToList();
 
    var grupos = from c in coches
                    orderby c.Marca descending 
                    group c by c.Marca into datosAgrupados
                    select new { Clave = datosAgrupados.Key, Datos = datosAgrupados };
 
    foreach (var grupo in grupos)
    {
        Console.WriteLine("***    {0}     ***", grupo.Clave);
 
        foreach (var dato in grupo.Datos)
        {
            Console.WriteLine("     {0,-6} - {1,-8} - {2} ", dato.Modelo, dato.Color, dato.Carburante);
        }
    }
 
    Console.Read();
}


En la primera sentencia, que aparece comentada podemos ver el uso de group by, mediante Lambda. Justo debajo, tenemos el ejemplo de operador de consulta, en este hemos utilizado la sentencia into  (que estudiaremos un poco más adelante), para agregar una especie de variable local, que contendrá nuestros grupos y un tipo anónimo de salida, en el que hemos encapsulado las dos partes más importante del group by, la clave (Key) y los registros resultantes de la agrupación mediante esa clave, al que le hemos añadido el alias de Datos.

A la hora de imprimir los datos, simplemente recorremos un bucle anidado, que representa nuestro group by, que no viene a ser más que una colección de colecciones.

Con el siguiente resultado:















Ejemplo de agrupación por varios campos (Tipo anónimo)

En este caso, la agrupación la realizaremos por más de un campo, por lo que nuestra Key o clave estará formada por más de una propiedad. Para este menester nos podríamos crear una clase que definiera un tipo con los campos que necesitamos para la agrupación, pero lo más sencillo es tirar de los tipos anónimos, que ya estudiamos con anterioridad y que nos ahorrarán escribir código.

En este ejemplo agruparemos por las propiedades Marca y Carburante:




Código:


static void Main(string[] args)
{
 
    var coches = new List<Automovil>
    {
        new Automovil { Marca = "Renault", Modelo = "Clio"  , Carburante = TipoCarburante.Gasolina, Color = Colors.Green },
        new Automovil { Marca = "Citroen", Modelo = "C3"    , Carburante = TipoCarburante.Diesel  , Color = Colors.Gray  },
        new Automovil { Marca = "Renault", Modelo = "Laguna", Carburante = TipoCarburante.Diesel  , Color = Colors.Black },
        new Automovil { Marca = "Renault", Modelo = "Megane", Carburante = TipoCarburante.Gasolina, Color = Colors.Blue  },
        new Automovil { Marca = "Citroen", Modelo = "C4"    , Carburante = TipoCarburante.Gasolina, Color = Colors.Black }
    };
 
    /// Lambda
    //var resultado = coches.GroupBy(c => new { c.Marca, c.Carburante }).ToList();
 
    var grupos = from c in coches
                    group c by new { c.Marca, c.Carburante } into datosAgrupados
                    select new { Clave = datosAgrupados.Key, Datos = datosAgrupados };
 
    foreach (var grupo in grupos)
    {
        Console.WriteLine("*** {0} - {1} ***", grupo.Clave.Marca, grupo.Clave.Carburante);
 
        foreach (var dato in grupo.Datos)
        {
            Console.WriteLine("     {0,-6} - {1} ", dato.Modelo, dato.Color);
        }
    }
 
    Console.Read();
}


El código es muy similar al caso de agrupación simple, con la diferencia de que utilizamos un tipo anónimo para realizar la agrupación compuesta por las propiedades Marca y Carburante. Esto queda también marcado a la hora de recorrer los datos ya que nuestra Key es compuesta y ahora tiene 2 propiedades.


Resultado:














Como dijimos al principio la cláusula group by, forma partes del grupo de proyección o transformación, por lo que si nos fijamos en el la sentencia que estaba comentada en el ejemplo anterior y que utilizaba Lambdas, en ella no aparece para nada el operador select:


var resultado = coches.GroupBy(c => new { c.Marca, c.Carburante }).ToList();

Esto viene a evidenciar que ella haría sola la transformación y que para nada requeriría la ayuda del operador select. En nuestro ejemplo anterior, lo utilizamos para generar un tipo anónimo, que hiciera más legible el código de impresión y los bucles, pero perfectamente está sentencia podría haberse reducido como se muestra:


static void Main(string[] args)
{
 
    var coches = new List<Automovil>
    {
        new Automovil { Marca = "Renault", Modelo = "Clio"  , Carburante = TipoCarburante.Gasolina, Color = Colors.Green },
        new Automovil { Marca = "Citroen", Modelo = "C3"    , Carburante = TipoCarburante.Diesel  , Color = Colors.Gray  },
        new Automovil { Marca = "Renault", Modelo = "Laguna", Carburante = TipoCarburante.Diesel  , Color = Colors.Black },
        new Automovil { Marca = "Renault", Modelo = "Megane", Carburante = TipoCarburante.Gasolina, Color = Colors.Blue  },
        new Automovil { Marca = "Citroen", Modelo = "C4"    , Carburante = TipoCarburante.Gasolina, Color = Colors.Black }
    };
 
    /// Lambda
    //var resultado = coches.GroupBy(c => new { c.Marca, c.Carburante }).ToList();
 
    var grupos = from c in coches
                    group c by new { c.Marca, c.Carburante };
 
    foreach (var grupo in grupos)
    {
        Console.WriteLine("*** {0} - {1} ***", grupo.Key.Marca, grupo.Key.Carburante);
 
        foreach (var dato in grupo)
        {
            Console.WriteLine("     {0,-6} - {1} ", dato.Modelo, dato.Color);
        }
    }
 
    Console.Read();
}




Agrupación por varios campos (IEqualityComparer)

Bueno pues vamos a añadir un toque un poco de exclusividad al post, con un ejemplo de agrupación utilizando IEqualityComparer, así incorporamos el primer post de este tipo (ya que no he sido capaz de encontrar ninguno en toda la red) y le otorgamos un poquito más de valor a mi última entrada.

Normalmente es bastante extraño necesitar un IEqualityComparer, para realizar una agrupación, habitualmente se suelen emplear más en operadores como: Unión, Distinct, Intersect, etc., y que investigaremos un poquito más adelante. Me gusta adicionar estos ejemplos de casos extremos, porque te permiten pensar un poco más de lo normal, y si eres capaz de comprenderlos te hacen estar un poco más cerca de la solución de dilemas futuros.


Pongamos que tenemos las siguientes clases:


public class PersonaReducida
{
    public string    ID              { get; set; }
    public DateTime  FechaNacimiento { get; set; }
    public Direccion Domicilio       { get; set; }  
}
 
 
 
public class Direccion
{
    public string Calle     { get; set; }
    public int    Numero    { get; set; }
    public string Provincia { get; set; }
}

Lo único peculiar de estas clases, es que PersonaReducida, tiene una propiedad de tipo definido Direccion.


Ahora pongámonos en la tesitura de que nuestras reglas de negocio nos obligan a realizar una agrupación por la propiedad Domicilio de tipo Direccion, como muestra el siguiente código:

Code:
static void Main(string[] args)
{
 
    var personas = new List<PersonaReducida>() 
    {
        new PersonaReducida 
        { 
            ID = "11111111A",
            FechaNacimiento = new DateTime(1970, 11, 21),
            Domicilio = new Direccion { Calle = "Calle Uno", Numero = 1, Provincia = "Madrid" }
        },
        new PersonaReducida 
        { 
            ID = "22222222B",
            FechaNacimiento = new DateTime(1980, 01, 15),
            Domicilio = new Direccion { Calle = "Calle Uno", Numero = 1, Provincia = "Madrid" }
        },
        new PersonaReducida 
        { 
            ID = "33333333C",
            FechaNacimiento = new DateTime(2000, 03, 02),
            Domicilio = new Direccion { Calle = "Calle Dos", Numero = 2, Provincia = "Toledo" }
        }
    };
 
 
    var grupos = personas.GroupBy(p => p.Domicilio).ToList();
 
 
    foreach (var grupo in grupos)
    {
        Console.WriteLine("*** {0} - {1} - {2} ***", grupo.Key.Calle, grupo.Key.Numero, grupo.Key.Provincia);
 
        foreach (var dato in grupo)
        {
            Console.WriteLine("     {0,-6} - {1,-8}", dato.ID, dato.FechaNacimiento);
        }
    }
 
    Console.Read();
}


Como se puede apreciar, podríamos pensar que las dos primeras personas tienen el mismo domicilio y deberían aparecer dentro del mismo grupo, pero si ejecutamos el código, este es el resultado:












El resultado, es que se nos crea un grupo diferente para cada una de ellas. Esto es debido a que estamos realizando la agrupación por un tipo definido por nosotros mismos, ósea por una clase, que aunque sus propiedades contengan los mismos valores, sus reglas de comparación se rigen por sus referencias y no por el valor de sus propiedades.

En los ejemplos anteriores la propiedad por la que agrupábamos era una clase de tipo string, un struct o un tipo anónimo, que  al contrario que las clases comunes, basan sus reglas de comparación por su valor y no por su referencia. Denotar aquí que aunque el tipo string sea una clase, esta es una clase especial ya que es inmutable, y tienen la peculiaridad de re-instanciarse cada vez que su valor cambia. Las estructuras y los delegados también son inmutables. Los tipos anónimos, tienen la singularidad de observar el valor de sus propiedades para denotar si dos objetos son iguales. Pero bueno esto nos daría para otro post, si hay alguien interesado que me lo pida y lo haré encantadísimo.

Después de este pequeño inciso, continuamos con lo  nuestro …

Teníamos el problema que aparentemente para nosotros la primera persona y la segunda tenían el mismo, pero al ser referencias diferentes, estaban en grupos diferentes. Para arreglar esto echaremos mano de nuestro IEqualityComparer y generaremos una forma de comparación compleja y personalizada para nuestro problema del tipo Direccion, del que facilitaremos una instancia al método extensor GroupBy.

Generamos la clase que hereda de IEqualityComparer:


public class DireccionEqualityComparer : IEqualityComparer<Direccion>
{
 
        #region IEqualityComparer Members
 
    public bool Equals(Direccion x, Direccion y)
    {
        return x.Calle     == y.Calle  && 
               x.Numero    == y.Numero && 
               x.Provincia == y.Provincia;
    }
 
    public int GetHashCode(Direccion obj)
    {
        return obj.Calle    .GetHashCode() + 
               objCalle    .GetHashCode() + 
               obj.Provincia.GetHashCode();
    }
 
        #endregion
}

Lo que hemos hecho, simplemente es indicar que para que dos objetos de tipo Direccion sean iguales, tienen que tener el mismo valor sus propiedades Calle, Numero y Provincia.


Volvemos a nuestro código, pero ahora con una pequeña diferencia:



Code:
static void Main(string[] args)
{
 
    var personas = new List<PersonaReducida>() 
    {
        new PersonaReducida 
        { 
            ID = "11111111A",
            FechaNacimiento = new DateTime(1970, 11, 21),
            Domicilio = new Direccion { Calle = "Calle Uno", Numero = 1, Provincia = "Madrid" }
        },
        new PersonaReducida 
        { 
            ID = "22222222B",
            FechaNacimiento = new DateTime(1980, 01, 15),
            Domicilio = new Direccion { Calle = "Calle Uno", Numero = 1, Provincia = "Madrid" }
        },
        new PersonaReducida 
        { 
            ID = "33333333C",
            FechaNacimiento = new DateTime(2000, 03, 02),
            Domicilio = new Direccion { Calle = "Calle Dos", Numero = 2, Provincia = "Toledo" }
        }
    };
 
 
    var grupos = personas.GroupBy(p => p.Domicilio, new DireccionEqualityComparer()).ToList();
 
 
    foreach (var grupo in grupos)
    {
        Console.WriteLine("*** {0} - {1} - {2} ***", grupo.Key.Calle, grupo.Key.Numero, grupo.Key.Provincia);
 
        foreach (var dato in grupo)
        {
            Console.WriteLine("     {0,-6} - {1,-8}", dato.ID, dato.FechaNacimiento);
        }
    }
 
 
    Console.Read();
}


Ahora le decimos la manera en que queremos que diferencie los direcciones:


var grupos = personas.GroupBy(p => p.Domicilio, new DireccionEqualityComparer()).ToList();

Y ahora el resultado si es el esperado:












Un apunte a tener en cuenta, como podéis observar en este ejemplo, no hemos utilizado sintaxis de expresión de consulta, y es que para este tipo de patrón en el que se utilizan sobrecargas de los métodos extensores, solo tiene cabida el uso de su expresión lambda.



Uso práctico de GroupBy

Dentro de nuestro día a día y del trabajo diario, en este caso hablo del mío, una de las tareas que solemos realizar en ocasiones comúnmente, es la comprobación de registros duplicados, dentro de ficheros en ocasiones mastodónticos. En estos casos una simple qry de LinQ, nos hará todo el trabajo sucio.

Supongamos que tenemos un fichero .csv de ejemplo con 4 columnas como este:

































Lo dejo en texto por si alguien quisiera copiar los datos:


PROPIEDAD1;PROPIEDAD2;PROPIEDAD3;PROPIEDAD4
1;Dato1;10/01/2015;1000
2;Dato2;11/01/2015;1001
3;Dato3;12/01/2015;1002
4;Dato4;13/01/2015;1003
5;Dato5;14/01/2015;1004
6;Dato6;15/01/2015;1005
7;Dato7;16/01/2015;1006
9;Dato9;17/01/2015;1007
9;Dato9;18/01/2015;1008
10;Dato10;19/01/2015;1009
11;Dato11;20/01/2015;1010
12;Dato12;21/01/2015;1011
13;Dato13;22/01/2015;1012
14;Dato14;23/01/2015;1013
15;Dato15;24/01/2015;1014
16;Dato16;25/01/2015;1015
17;Dato17;26/01/2015;1016
18;Dato18;27/01/2015;1017
19;Dato19;28/01/2015;1018
20;Dato20;29/01/2015;1019
21;Dato21;30/01/2015;1020



Lo leemos y lo pasamos a una clase de negocio como esta:


public class MiclaseNegocio
{
    public int      Prop1 { get; set; }
    public string   Prop2 { get; set; }
    public DateTime Prop3 { get; set; }
    public double   Prop4 { get; set; }
}


Esta simple sentencia nos daría los registros duplicados dentro del fichero:


var duplicados = from r in registrosFichero
                 group r by new { r.Prop1, r.Prop2 } into agrupados
                 where agrupados.Count() > 1
                 select new { Clave = agrupados.Key, Datos = agrupados };