2009-12-02 48 views
33

Quiero mostrar el historial contable de un cliente en un DataGridView y quiero tener una columna que muestre el total acumulado de su saldo. La forma antigua en que lo hice fue obtener los datos, recorrer los datos y agregar filas al DataGridView uno a uno y calcular el total acumulado en ese momento. Cojo. Prefiero usar LINQ to SQL, o LINQ si no es posible con LINQ to SQL, para calcular los totales acumulados, así que puedo simplemente configurar DataGridView.DataSource en mis datos.LINQ to SQL y un total acumulado de resultados ordenados

Este es un ejemplo súper simplificado de lo que estoy rodando. Digamos que tengo la siguiente clase.

class Item 
{ 
    public DateTime Date { get; set; } 
    public decimal Amount { get; set; } 
    public decimal RunningTotal { get; set; } 
} 

Me gustaría tener un L2S, o LINQ, indicación de que podría generar resultados que se ven así:

Date  Amount RunningTotal 
12-01-2009  5   5 
12-02-2009  -5   0 
12-02-2009  10   10 
12-03-2009  5   15 
12-04-2009 -15   0 

Tenga en cuenta que puede haber múltiples elementos con la misma fecha (12-02-2009) Los resultados deben ordenarse por fecha antes de calcular los totales acumulados. Supongo que esto significa que necesitaré dos declaraciones, una para obtener los datos y ordenarlos, y otra para realizar el cálculo del total acumulado.

Esperaba que Aggregate hiciera el truco, pero no funciona como esperaba. O tal vez simplemente no pude resolverlo.

Este question parecía estar yendo después de lo mismo que yo quería, pero no veo cómo la respuesta aceptada/única resuelve mi problema.

¿Alguna idea sobre cómo llevarlo a cabo?

Editar Peinar las respuestas de Alex y DOK, esto es lo que terminó con:

decimal runningTotal = 0; 
var results = FetchDataFromDatabase() 
    .OrderBy(item => item.Date) 
    .Select(item => new Item 
    { 
     Amount = item.Amount, 
     Date = item.Date, 
     RunningTotal = runningTotal += item.Amount 
    }); 
+0

Gracias por la nueva herramienta! : RunningTotal = runningTotal + = item.Amount –

+0

¿Esta solución no obligará a la ejecución a estar en el cliente? (es decir, tiene que desplegar todo el conjunto de resultados para obtener la respuesta correcta?) - parece que algo así sería mucho más eficaz si se realizara en el servidor SQL ... – BrainSlugs83

+1

Usando una variable externa a la consulta ¡es muy peligroso! Como la variable 'results' es del tipo' IEnumerable', su ** ejecución se aplazará ** hasta más tarde. Si cambia el valor de 'runningTotal' antes de eso, su consulta resultante ya no será correcta. Para estar seguro necesita enumerarlo de inmediato (para listar o ordenar). No veo nada malo aquí con el uso de un simple bucle 'foreach'. – Alexey

Respuesta

35

El uso de cierres y método anónimo:

List<Item> myList = FetchDataFromDatabase(); 

decimal currentTotal = 0; 
var query = myList 
       .OrderBy(i => i.Date) 
       .Select(i => 
          { 
          currentTotal += i.Amount; 
          return new { 
              Date = i.Date, 
              Amount = i.Amount, 
              RunningTotal = currentTotal 
             }; 
          } 
        ); 
foreach (var item in query) 
{ 
    //do with item 
} 
+1

¡Desearía poder marcarlos a ambos como la respuesta! Tu respuesta fue fácil de entender y coincide con mi ejemplo. Me gusta cómo DOK incrementa el 'currentTotal' en línea, durante la asignación, sin embargo. – Ecyrb

+0

+1 SOF debe tener opciones para marcar múltiples respuestas. –

+4

He intentado esto con LINQ a Entidades (EF) y obtener un "Una expresión lambda con un cuerpo de la declaración no puede ser convertido en un árbol de expresión" error de compilación. ¿Es eso particular para EF en comparación con L2O? –

25

¿Qué tal esto: (crédito va a this source)

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 

namespace ConsoleApplication1 
{ 
    class Program 
    { 
     delegate string CreateGroupingDelegate(int i); 

     static void Main(string[] args) 
     { 
      List<int> list = new List<int>() { 1, 2, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 69, 2007}; 
      int running_total = 0; 

      var result_set = 
       from x in list 
       select new 
       { 
        num = x, 
        running_total = (running_total = running_total + x) 
       }; 

      foreach (var v in result_set) 
      { 
       Console.WriteLine("list element: {0}, total so far: {1}", 
        v.num, 
        v.running_total); 
      } 

      Console.ReadLine(); 
     } 
    } 
} 
+0

¡Gracias! Me gusta cómo asignas 'running_total' en línea. Eso es bastante astuto. – Ecyrb

+3

'running_total = (running_total = running_total + x)' => Mind blown. Seguramente recordaré esto para la próxima vez :) –

+1

¡genial! También funciona con tipos de objetos fuertemente definidos, no solo con tipos anónimos – hagensoft

1

agregada puede ser utilizado para obtener un total acumulado así:

var src = new [] { 1, 4, 3, 2 }; 
var running = src.Aggregate(new List<int>(), (a, i) => { 
    a.Add(a.Count == 0 ? i : a.Last() + i); 
    return a; 
}); 
5

En caso de que no ha recibido ninguna respuesta, tengo una solución que he utilizado en mis proyectos. Esto es bastante similar a un grupo con particiones Oracle. La clave es hacer que la cláusula where en el total acumulado coincida con la lista orig y luego agruparla por fecha.

var itemList = GetItemsFromDBYadaYadaYada(); 

var withRuningTotals = from i in itemList  
         select i.Date, i.Amount,  
           Runningtotal = itemList.Where(x=> x.Date == i.Date). 
                 GroupBy(x=> x.Date). 
                 Select(DateGroup=> DateGroup.Sum(x=> x.Amount)).Single(); 
+0

Interesante. Parece que esto descargaría datos a la base de datos correctamente. – BrainSlugs83

0
using System; 
using System.Linq; 
using System.Collections.Generic; 

public class Program 
{ 
    public static void Main() 
    { 
     var list = new List<int>{1, 5, 4, 6, 8, 11, 3, 12}; 

     int running_total = 0; 

     list.ForEach(x=> Console.WriteLine(running_total = x+running_total)); 
    } 
}