martes, 19 de abril de 2016

Operadores de Agregación



Los Aggregate Operators, nos permiten realizar operaciones matemáticas de una temática simple sobre los elementos que forman una colección. Normalmente el resultado de estos, es un valor numérico, aunque no siempre es obligatorio.
La ejecución de todos estos operadores es inmediata, y se realiza justo en el momento de realizar la llamada, por lo que ninguno de ellos tiene ejecución diferida o perezosa.

En este conjunto de operadores, se diferencian tres tipos. Un primer tipo muy simple, compuesto por Count y LongCount. Un segundo grupo también bastante sencillo compuesto por Max, Min, Sum y Average y un tercero compuesto por el operador Aggregate, más complejo y potente. El primer grupo, posee sobrecargas sin parámetros, para secuencias numéricas sencillas y sobrecargas con filtrado (parámetro Func<T, bool>), que purifican la secuencia antes de aplicar el cálculo. Los métodos correspondientes al segundo grupo, poseen un número bastante considerable de sobrecargas, que van en concordancia con el tipo de datos sobre el que se aplica el cálculo, normalmente int, int?, double, doublé?, decimal, decimal?, long y long? En afinidad con su delegado genérico Func<T, int>, Func<T, int?>, Func<T, double>


Los operadores de este tipo, tales como: Sum, Average, Min, Max, etc., se podrían realizar perfectamente con el operador Aggregate, pero parece ser que el equipo de LinQ, los incluyó por razones de facilidad de lectura y compresión de código. Esto lo demostraremos un poco más a fondo al final de este post.



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



Count y LongCount

Los métodos extensores, Count y LongCount, retornan el número de elementos que contiene una secuencia. La diferencia entre ellos, es que Count se utiliza para colecciones de elementos estándar y LongCount, para colecciones de un número de elementos mucho más elevado. El operador Count devuelve un objeto de tipo int (Int32), por lo que no podrá ser utilizado por secuencias que contengan más de 2.147.483.647 elementos, para colecciones mayores tendremos que emplear el LongCont que devuelve un objeto de tipo Long (Int64) y que permitirá contabilizar elementos para colecciones de hasta 9.223.372.036.854.775.807. Ahí queda eso.


Vamos con sus firmas:


public static int Count<TSource>(this IEnumerable<TSource> source);
public static int Count<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
 
public static long LongCount<TSource>(this IEnumerable<TSource> source);
public static long LongCount<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate);


Ambos tienen 2 sobrecargas cada uno, la segunda, admite un predicate, por si queremos realizar el recuento, no de la secuencia completa, sino de un grupo filtrado. Este predicate, indicará las condiciones del filtrado.


Vamos a ver unos ejemplos:


static void Main(string[] args)
{
    var elementos = new List<Elemento>()
    {
        new Elemento{ ID = 0, Amount = 10000},
        new Elemento{ ID = 1, Amount =  1000},
        new Elemento{ ID = 2, Amount =  2000},
        new Elemento{ ID = 3, Amount =  3000}
    };
 
 
    var numCorto = elementos.Count();
    var numLargo = elementos.LongCount();
 
    var numCortoFiltrado = elementos.Count    (e => e.Amount >= 2000);
    var numLargoFiltrado = elementos.LongCount(e => e.Amount >= 2000);
 
 
    Console.WriteLine($"{nameof(numCortoFiltrado)} es de tipo {numCortoFiltrado.GetType().Name}");
    Console.WriteLine($"{nameof(numLargoFiltrado)} es de tipo {numLargoFiltrado.GetType().Name}");
 
    Console.Read();
}


El resultado es exactamente el mismo, solo cambiará el tipo de dato devuelto. El operador LongCount, solo lo utilizaremos cuando sepamos que las secuencias son extremadamente grandes. Normalmente, en un trabajo diario, no suele emplearse.




NOTA


No debemos confundir el método extensor Count() de la interfaz IEnumerable<T>, con la propiedad Count de Interfaz ICollection<T> e IList<T>. La segunda ya existía antes de la aparición de LinQ, y está solo se puede aplicar sobre colecciones que implementen esta interfaz. El método extensor Count() tiene un abanico mucho más amplio de uso, ya que es mayor el número de colecciones en el Framework que implementan IEnumerable<T> que ICollection<T>. Cuando utilizamos este operador, si lo aplicamos sobre secuencias de los tipos ICollection<T> e IList<T>, estos simplemente consultarán su propiedad Count, haciendo la ejecución prácticamente inmediata.   








Sum

El operador sum, devuelve la suma de una secuencia de elementos. Estos elementos pueden ser nulos.

Tiene 20 sobrecargas, las cuales están divididas en 2 grupos bien diferenciados, y que en el momento de escribir el código, nos puede parecer que solo tiene 2, una por cada grupo.


El primero, formado por 10 sobrecargas (con un solo parámetro), que se aplican sobre una secuencia de objetos del mismo tipo (siempre numérico) que devuelve:


public static long     Sum(this IEnumerable<long>     source);
public static long?    Sum(this IEnumerable<long?>    source);
public static float?   Sum(this IEnumerable<float?>   source);
public static double   Sum(this IEnumerable<double>   source);
public static double?  Sum(this IEnumerable<double?>  source);
public static decimal? Sum(this IEnumerable<decimal?> source);
public static decimal  Sum(this IEnumerable<decimal>  source);
public static float    Sum(this IEnumerable<float>    source);
public static int?     Sum(this IEnumerable<int?>     source);
public static int      Sum(this IEnumerable<int>      source);



Dicho con otras palabras, este operador se aplicará siempre sobre colecciones de elementos numéricos y devolverá la suma de los mismos en un objeto del mismo tipo numérico que el referente a cualquier elemento del campo de la colección.

Unos ejemplitos:


static void Main(string[] args)
{
    var elementos = new List<Elemento>()
    {
        new Elemento{ ID = 0, Amount = 10000},
        new Elemento{ ID = 1, Amount =  1000},
        new Elemento{ ID = 2, Amount =  2000},
        new Elemento{ ID = 3, Amount =  3000}
    };
 
    int[] cifras = new int[] { 1000, 2000, 3000 };
 
 
    int resultadoCifras = cifras.Sum();
 
    var sumAmount = elementos.Select(a => a.Amount).Sum();
 
    Console.WriteLine($"{nameof(resultadoCifras)} = {resultadoCifras}");
    Console.WriteLine($"{nameof(sumAmount)}       = {sumAmount}"      );
 
    Console.Read();
}


Como se puede apreciar las llamadas no llevan ningún parámetro.












El Segundo grupo, también está formado por 10 sobrecargas, en este caso son métodos genéricos, con 2 parámetros cada una, que contienen un segundo parámetro selector, donde le indicaremos al método el campo o la propiedad de nuestra clase de la que queremos servirnos para realizar la suma de elementos:


public static float?   Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, float?>   selector);
public static double?  Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, double?>  selector);
public static decimal  Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal>  selector);
public static decimal? Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal?> selector);
public static double   Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, double>   selector);
public static int?     Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, int?>     selector);
public static long?    Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, long?>    selector);
public static float    Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, float>    selector);
public static long     Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, long>     selector);
public static int      Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, int>      selector);


Vamos a plasmarlo en un ejemplo;


static void Main(string[] args)
{
    var elementos = new List<Elemento>()
    {
        new Elemento { ID = 0, Amount = 10000},
        new Elemento { ID = 1, Amount =  1000},
        new Elemento { ID = 2, Amount =  2000},
        new Elemento { ID = 3, Amount =  3000}
    };
 
    var sumAmount = elementos.Sum(a => a.Amount); 
 
    Console.WriteLine($"{nameof(sumAmount)} = {sumAmount}"      );
 
    Console.Read();
}


Como se puede distinguir, añadimos un parámetro de tipo Func<Elemento, int>, en el que le indicamos el campo que queremos que emplee para realizar la suma.


Si enfrentamos las 2 opciones:


var sumAmount = elementos.Select(a => a.Amount).Sum();
 
var sumAmount = elementos.Sum(a => a.Amount); 


Vemos que en la primera, nosotros hacemos el trabajo de selección con un delegado del mismo tipo, mientras que en la segunda, al recibir ya esté parámetro él hace el trabajo por nosotros.

En caso de que la columna con la que operar sea de tipo Nullable y ésta esté compuesta por valores nulos, el resultado será 0 por cada valor null que se encuentre:


static void Main(string[] args)
{
    var elementos = new List<Elemento>()
    {
        new Elemento { ID = 0, Amount = null},
        new Elemento { ID = 1, Amount = null},
        new Elemento { ID = 2, Amount = null},
        new Elemento { ID = 3, Amount = null}
    };
 
    var sumAmount = elementos.Sum(a => a.Amount); 
 
    Console.WriteLine($"{nameof(sumAmount)} = {sumAmount}"      );
 
    Console.Read();
}












Min y Max

El operador Min y Max, tienen las mismas peculiaridades que Sum, pero evidentemente cumpliendo el cometido de sus respectivos nombres, buscar los valores máximos y mínimos dentro de una secuencia.


Empezaremos con las firmas de sus métodos extensores:


public static double?   Max(this IEnumerable<double?>  source);
public static double    Max(this IEnumerable<double>   source);
public static long?     Max(this IEnumerable<long?>    source);
public static long      Max(this IEnumerable<long>     source);
public static int?      Max(this IEnumerable<int?>     source);
public static float     Max(this IEnumerable<float>    source);
public static float?    Max(this IEnumerable<float?>   source);
public static int       Max(this IEnumerable<int>      source);
public decimal          Max(this IEnumerable<decimal>  source);
public decimal?         Max(this IEnumerable<decimal?> source);
 
public static long?     Min(this IEnumerable<long?>    source);
public static int?      Min(this IEnumerable<int?>     source);
public static int       Min(this IEnumerable<int>      source);
public static float?    Min(this IEnumerable<float?>   source);
public static double    Min(this IEnumerable<double>   source);
public static double?   Min(this IEnumerable<double?>  source);
public static decimal   Min(this IEnumerable<decimal>  source);
public static decimal?  Min(this IEnumerable<decimal?> source);
public static long      Min(this IEnumerable<long>     source);
public static float     Min(this IEnumerable<float>    source);
 
 
 
 
public static int       Max<TSource>(this IEnumerable<TSource> source, Func<TSource, int>      selector);
public static int?      Max<TSource>(this IEnumerable<TSource> source, Func<TSource, int?>     selector);
public static long      Max<TSource>(this IEnumerable<TSource> source, Func<TSource, long>     selector);
public static long?     Max<TSource>(this IEnumerable<TSource> source, Func<TSource, long?>    selector);
public static float     Max<TSource>(this IEnumerable<TSource> source, Func<TSource, float>    selector);
public static float?    Max<TSource>(this IEnumerable<TSource> source, Func<TSource, float?>   selector);
public static double    Max<TSource>(this IEnumerable<TSource> source, Func<TSource, double>   selector);
public static double?   Max<TSource>(this IEnumerable<TSource> source, Func<TSource, double?>  selector);
public static decimal   Max<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal>  selector);
public static decimal?  Max<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal?> selector);
 
public static long?     Min<TSource>(this IEnumerable<TSource> source, Func<TSource, long?>    selector);
public static decimal?  Min<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal?> selector);
public static decimal   Min<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal>  selector);
public static float     Min<TSource>(this IEnumerable<TSource> source, Func<TSource, float>    selector);
public static double?   Min<TSource>(this IEnumerable<TSource> source, Func<TSource, double?>  selector);
public static int?      Min<TSource>(this IEnumerable<TSource> source, Func<TSource, int?>     selector);
public static int       Min<TSource>(this IEnumerable<TSource> source, Func<TSource, int>      selector);
public static float?    Min<TSource>(this IEnumerable<TSource> source, Func<TSource, float?>   selector);
public static long      Min<TSource>(this IEnumerable<TSource> source, Func<TSource, long>     selector);
public static double    Min<TSource>(this IEnumerable<TSource> source, Func<TSource, double>   selector);


Y ahora vamos con un ejemplo:


static void Main(string[] args)
{
    int[] cifras = new int[] { 1000, 2000, 3000 };
 
 
    int maxCifras = cifras.Max();
    int minCifras = cifras.Min();
 
 
 
    Console.WriteLine($"{nameof(maxCifras) } = {maxCifras }");
    Console.WriteLine($"{nameof(minCifras) } = {minCifras }");
 
    Console.Read();
}











Un ejemplo un poco más complicado:


static void Main(string[] args)
{
    var elementos = new List<Elemento>()
    {
        new Elemento { ID = 0, Amount = 10000},
        new Elemento { ID = 1, Amount =  1000},
        new Elemento { ID = 2, Amount =  2000},
        new Elemento { ID = 3, Amount =  3000}
    };
 
    var maxAmountPorSelect = elementos.Max(a => a.Amount);
    var minAmountPorSelect = elementos.Min(a => a.Amount);
 
    var maxAmountPorCampo = elementos.Max(a => a.Amount);
    var minAmountPorCampo = elementos.Min(a => a.Amount);
 
    Console.WriteLine($"{nameof(maxAmountPorSelect)} = {maxAmountPorSelect}");
    Console.WriteLine($"{nameof(minAmountPorSelect)} = {minAmountPorSelect}");
    Console.WriteLine($"{nameof(maxAmountPorCampo) } = {maxAmountPorCampo }");
    Console.WriteLine($"{nameof(minAmountPorCampo) } = {minAmountPorCampo }");
 
    Console.Read();
}













Average

Misma fórmula y mismo comportamiento y tratamiento que para sus hermanos Sum, Max y Min, pero para hallar la media.

En este caso, los tipos de devolución serán siempre compatibles con datos decimales.


Firmas:


public static float    Average(this IEnumerable<float>    source);
public static double?  Average(this IEnumerable<long?>    source);
public static float?   Average(this IEnumerable<float?>   source);
public static double   Average(this IEnumerable<double>   source);
public static double   Average(this IEnumerable<int>      source);
public static decimal  Average(this IEnumerable<decimal>  source);
public static decimal? Average(this IEnumerable<decimal?> source);
public static double   Average(this IEnumerable<long>     source);
public static double?  Average(this IEnumerable<double?>  source);
public static double?  Average(this IEnumerable<int?>     source);

public static decimal  Average<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal>  selector);
public static decimal? Average<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal?> selector);
public static double?  Average<TSource>(this IEnumerable<TSource> source, Func<TSource, int?>     selector);
public static double   Average<TSource>(this IEnumerable<TSource> source, Func<TSource, long>     selector);
public static double?  Average<TSource>(this IEnumerable<TSource> source, Func<TSource, long?>    selector);
public static float    Average<TSource>(this IEnumerable<TSource> source, Func<TSource, float>    selector);
public static float?   Average<TSource>(this IEnumerable<TSource> source, Func<TSource, float?>   selector);
public static double   Average<TSource>(this IEnumerable<TSource> source, Func<TSource, double>   selector);
public static double?  Average<TSource>(this IEnumerable<TSource> source, Func<TSource, double?>  selector);
public static double   Average<TSource>(this IEnumerable<TSource> source, Func<TSource, int>      selector);



Un ejemplo simple:


static void Main(string[] args)
{
    int[] cifras = new int[] { 1000, 2000, 3000 };
 
    var mediaCifras = cifras.Average();
 
    Console.WriteLine($"{nameof(mediaCifras) } = {mediaCifras }");
 
    Console.Read();
}










Uno un poco más complejo:


static void Main(string[] args)
{
    var elementos = new List<Elemento>()
    {
        new Elemento { ID = 0, Amount = 10000},
        new Elemento { ID = 1, Amount =  1000},
        new Elemento { ID = 2, Amount =  2000},
        new Elemento { ID = 3, Amount =  3000}
    };
 
    var mediaPorSelect = elementos.Max(a => a.Amount);
 
    var mediaPorCampo = elementos.Max(a => a.Amount);
 
 
    Console.WriteLine($"{nameof(mediaPorSelect)} = {mediaPorSelect}");
    Console.WriteLine($"{nameof(mediaPorCampo) } = {mediaPorCampo}" );
 
 
    Console.Read();
}













Aggregate

El método extensor Aggregate,  principalmente se utiliza para aplicar una función de acumulación a una colección.


Vamos a ver sus firmas:


public static TSource Aggregate<TSource>(this IEnumerable<TSource> source, Func<TSource, TSource, TSource> func);
public static TAccumulate Aggregate<TSource, TAccumulate>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func);
public static TResult Aggregate<TSource, TAccumulate, TResult>(this IEnumerable<TSource> source, TAccumulate seed, Func<TAccumulate, TSource, TAccumulate> func, Func<TAccumulate, TResult> resultSelector);


Primera Sobrecarga


En su parte más simple, el operador Aggregate realiza su tarea llamando al parámetro (Func<TSource, TSource, TSource> func) por cada uno de los elementos del parámetro source. El parámetro (Func<TSource, TSource, TSource> func) especifica la acción de acumulación.


Vamos a ver un ejemplo muy simple, que mostraría  tabulados los datos de un array de strings:


static void Main(string[] args)
{
 
    string[] nombres = { "Miguel", "Luis", "Gabriel", "Laura", "Maria del Carmen" };

    string resultado = nombres.Aggregate((result, next) => result += string.Format("\r\n{0:-16}", next));
 
    /// ESTO SERÍA LO QUE HARÍA LA FUNCIÓN
//string result = string.Empty;
 
//foreach (var next in nombres)
//{
//    result += string.Format("\r\n{0:-16}", next);
//}


    Console.WriteLine(resultado);
 
    Console.Read();
}












Segunda Sobrecarga


Esta segunda sobrecarga, es básicamente igual a la primera, pero con un parámetro TSource más, para indicarle el valor de inicialización que queremos para nuestro acumulador.


class Program
{
    static void Main(string[] args)
    {
 
        string[] nombres = { "Miguel", "Luis", "Gabriel", "Laura", "Maria del Carmen" };

        string resultado = nombres.Aggregate("Dato Inicial", (result, next) => result += string.Format("\r\n{0:-16}", next));
 
        Console.WriteLine(resultado);
 
        Console.Read();
    }
}












Tercera sobrecarga


Esta úlitma sobrecarga, tiene las mismas características de las anteriores, pero añade la alternativa de configurar el formato de salida, mediante el parámetro Func<TAccumulate, TResult> resultSelector.

Vamos a ver un ejemplo:


class Program
{
    static void Main(string[] args)
    {
 
        string[] nombres = { "Miguel", "Luis", "Gabriel", "Laura", "Maria del Carmen" };
 
        string resultado = nombres.Aggregate("Dato Inicial", 
                                                (result, next) => result += string.Format("\r\n{0:-16}", next),
                                                a => $"{a} \r\n --> Total caracteres:  {a.Length} "
                                                );
 
        Console.WriteLine(resultado);
 
        Console.Read();
    }
}













Bastante fácil.


Otro ejemplo un poquito más complicado, pero que nos puede ayudar a entenderlo un pelín mejor:


class Program
{
    static void Main(string[] args)
    {
 
        string[] nombres = { "Miguel", "Luis", "Gabriel", "Laura", "Maria del Carmen" };
 
        var separador = new string[] { "\r\n" };
 
        var resultado = nombres.Aggregate("Dato Inicial", 
                                                (result, next) => result += string.Format("\r\n{0:-16}", next),
                                                // Creamos un tipo anónimo por cada elemento
                                                a => a.Split(separador, StringSplitOptions.None).Select(b => new { Dato = b, Len = b.Length })
                                                );
 
        resultado.ToList().ForEach(r => Console.WriteLine(r));
 
        Console.Read();
    }
}


Importante ver la utilidad que damos al último parámetro de esta sobrecarga, particionando el string de resultado, para generarnos una colección de tipos anónimos con un campo descripción y la longitud de caracteres de cada uno.













La guinda del pastel


Siempre intento dar a mis post algo de originalidad y sobre todo de definición en contraste con otros ejemplos de la red, y para ello y como adelantaba al principio, voy a generar el resultado de los operadores Sum, Min/Max y Average, utiliznado el overador Aggregate:


SUM


static void Main(string[] args)
{
 
    int[] cifras = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
 
    var sum = cifras.Aggregate((total, next) => total += next);
 
    Console.WriteLine($"El valor de {nameof(sum)} es {sum}");
 
    Console.Read();
}











MIN


static void Main(string[] args)
{
 
    int[] cifras = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
 
    var min = cifras.Aggregate((total, next) => next < total ? next : total);
 
    Console.WriteLine($"El valor de {nameof(min)} es {min}");
 
    Console.Read();
}













MAX


static void Main(string[] args)
{
 
    int[] cifras = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
 
    var max = cifras.Aggregate((total, next) => next > total ? next : total);
 
    Console.WriteLine($"El valor de {nameof(max)} es {max}");
 
    Console.Read();
}











AVERAGE


static void Main(string[] args)
{
 
    int[] cifras = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
 
    var average = cifras.Aggregate(
                                        0, 
                                        (total, next) => total += next, 
                                        a => decimal.Parse(a.ToString()) / decimal.Parse(cifras.Count().ToString())
                                    );
 
    Console.WriteLine($"El valor de {nameof(average)} es {average}");
 
    Console.Read();
}

















No hay comentarios :

Publicar un comentario