viernes, 14 de octubre de 2011

Manipulando DataTables con LINQ

En un producto en el tengo la oportunidad de apoyar se necesitaba desplegar unas gráficas con la siguiente particularidad. Del data source solo se mostrarían los 5 valores mayores y el resto se acumularían y se desplegarían como un agregado.
Los datos en la aplicación se encuentran en un DataTable y en vez de ordenar con DataViews y sumar y seleccionar de la manera tradicional se me ocurrió utilizar LINQ para llevar a cabo la tarea. Combinando así tecnología de dos generaciones.
Anexo la clase con comentarios, no es una solución completamente genérica pero funciona para este producto y es reutilizable en toda la solución.
 
 public static class DataAcummulator
    {
        /// <summary>
        /// Generates a DataTable with only the top values and the 
        /// rest are aggregated as a single row
        /// </summary>
        /// <param name="source">Original data source. Will not be modified</param>
        /// <param name="topValues">number of top values to include</param>
        /// <param name="ValueColumn">column to be used as selection criteria and to be accumulated</param>
        /// <param name="percentage">does the datatable contains a percentage column?</param>
        /// <returns></returns>
        public static DataTable GetAcummulatedData(DataTable source, int topValues, string ValueColumn,bool percentage)
        {
            //primero ordenamos los datos del datatable de manera ascendente
            //notese que sabemos que la columna va a ser de tipo Decimal
            var query = from data in source.AsEnumerable()
                        orderby data.Field<Decimal>(ValueColumn) descending
                        select data;
            //de la coleccion ordenada tomamos los primeros n valores
            var firstValues = query.Take(topValues);
 
            //calculamos el total de una columna que siempre aparece en esta aplicacion
            //ok, esto está hardcodeado pero dentro del scope funciona perfecto
            var restRecordsTotalValue = query.Skip(topValues).Sum(c => c.Field<int>("Quotes"));
            //notese que Sum pudiera cambiarse por algo más complejo en caso de requerise
            var restRecordsTotalPercentage = query.Skip(topValues).Sum(c => c.Field<Decimal>(ValueColumn));
            //clonamos la estructura de la tabla, recuerden que no vamos a modificar el source
            DataTable filteredTable = source.Clone();
            //se usa import row porque los otros rows ya están attacheados al source DataTable
            foreach (var row in firstValues)
            {
                filteredTable.ImportRow(row);
            }
            //creamos el row nuevo que va a contener los valores acumulados
            //de nuevo hay hard code de cosas que sabemos que son ciertas en este proyecto
            var othersRow = filteredTable.NewRow();
            othersRow["Value"] = "Others";
            othersRow["Quotes"] = restRecordsTotalValue;
            if (percentage)
            {
                othersRow["Percentage"] = restRecordsTotalPercentage.ToString("p");
            }
            filteredTable.Rows.Add(othersRow);
            //voila!
            return filteredTable;
        }
    }