44 votes

LINQ to SQL et un total courant sur des résultats ordonnés

Je souhaite afficher l'historique comptable d'un client dans un fichier DataGridView et je veux avoir une colonne qui affiche le total courant de leur solde. L'ancienne façon de procéder consistait à récupérer les données, à les parcourir en boucle et à ajouter des lignes au fichier DataGridView un par un et de calculer le total courant à ce moment-là. C'est nul. Je préférerais de loin utiliser LINQ to SQL, ou LINQ si ce n'est pas possible avec LINQ to SQL, pour calculer les totaux courants afin que je puisse simplement mettre DataGridView.DataSource à mes données.

Voici un exemple super-simplifié de ce que je cherche à obtenir. Disons que j'ai la classe suivante.

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

Je voudrais une déclaration L2S, ou LINQ, qui pourrait générer des résultats ressemblant à ceci :

   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

Notez qu'il peut y avoir plusieurs éléments avec la même date (12-02-2009). Les résultats doivent être triés par date avant que les totaux courants ne soient calculés. Je suppose que cela signifie que j'aurai besoin de deux instructions, une pour obtenir les données et les trier et une seconde pour effectuer le calcul du total courant.

J'espérais Aggregate ferait l'affaire, mais ça ne fonctionne pas comme je l'espérais. Ou peut-être que je n'ai pas réussi à le comprendre.

Ce site question semblait viser la même chose que moi, mais je ne vois pas comment la réponse acceptée/unique résout mon problème.

Des idées sur la façon d'y parvenir ?

Modifier En combinant les réponses d'Alex et de DOK, voici ce à quoi j'ai abouti :

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 votes

Merci pour ce nouvel outil ! : RunningTotal = runningTotal += item.Amount

0 votes

Cette solution ne va-t-elle pas forcer l'exécution sur le client ? (c.-à-d. qu'il doit extraire l'ensemble des résultats pour obtenir la bonne réponse) -- il semble que quelque chose comme cela serait beaucoup plus performant si cela était fait sur le serveur SQL...

2 votes

L'utilisation d'une variable externe à la requête est très dangereuse ! Parce que results La variable est de IEnumerable type, son l'exécution sera différée jusqu'à plus tard. Si vous modifiez la valeur de runningTotal avant cela, la requête résultante ne sera plus correcte. Pour être sûr, vous devez l'énumérer immédiatement (en liste ou en tableau). Je ne vois pas ce qu'il y a de mal à utiliser un simple foreach boucle.

42voto

Alex Bagnolini Points 7403

Utilisation des fermetures et de la méthode anonyme :

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
}

2 votes

J'aimerais pouvoir vous marquer tous les deux comme étant la réponse ! Votre réponse était facile à comprendre et correspondait à mon exemple. J'aime bien la façon dont DOK incrémente le currentTotal en ligne, pendant l'affectation, cependant.

8 votes

J'ai essayé avec Linq to Entities (EF) et j'ai obtenu une erreur de compilation "A lambda expression with a statement body cannot be converted to an expression tree". Ce problème est-il propre à EF et non à L2O ?

2 votes

Notez que cela finit par être faux si vous utilisez le résultat de la requête à plusieurs reprises. Cette question en est le résultat. Vous pouvez résoudre ce problème en ajoutant un .ToList() à la fin de la requête.

28voto

DOK Points 21175

Que pensez-vous de ceci : (le crédit va à cette 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 votes

Merci ! J'aime comment tu assignes running_total en ligne. C'est plutôt malin.

5 votes

running_total = (running_total = running_total + x) => Mind blown. Je m'en souviendrai sûrement la prochaine fois :)

1 votes

cool ! Cela fonctionne également avec les objets de type fort, et pas seulement avec les types anonymes.

7voto

DF5 Points 71

Au cas où cette question n'aurait pas encore reçu de réponse, j'ai une solution que j'ai utilisée dans mes projets. C'est assez similaire à un groupe partitionné Oracle. La clé est de faire en sorte que la clause where du total courant corresponde à la liste d'origine, puis de la regrouper par date.

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();

2voto

Aaron Anodide Points 6375

L'agrégat peut également être utilisé pour obtenir un total courant :

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;
});

0 votes

Mais comment appliquer le total courant à la collection originale ? L'OP demande un moyen de l'ajouter à chaque élément.

0voto

daydreamer Points 33
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));
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X