views:

397

answers:

2

Is it possible to sum hierarchical data using .NET's LINQ?

My data class looks like this:

class Node
{
    public decimal Amount;
    public IEnumerable<Node> Children { get; set; }
}

So I would have some data looks like this, but the tree could of course be arbitrarily deep.

var amounts = new Node
{
    Amount = 10;
    Children = new[]
    {
        new Node
        {
            Amount = 20
        },
        new Node
        {
            Amount = 30
        }
    }
};

It is possible sum all the amounts and get the result 60 with one simple LINQ query?

+10  A: 

You can do it with a higher order function:

Func<Node, decimal> summer = null;
summer = node => node.Amount + 
                 (node.Children == null ? 0m : node.Children.Sum(summer));
decimal total = summer(amounts);

Note that if you can ensure that node.Children will never be null, summer can be simpler:

summer = node => node.Amount + node.Children.Sum(summer);

Alternatively, you could use the null coalescing operator:

summer = node => node.Amount + 
                 (node.Children ?? Enumerable.Empty<Node>()).Sum(summer);

Of course you could put this into a separate method instead:

static decimal SumNodes(Node node)
{
    return node.Amount + 
        (node.Children ?? Enumerable.Empty<Node>())
            .Sum((Func<Node, decimal>)SumNodes);
}

Note the ugliness here is due to an ambiguity in method group conversions. Method groups don't get much love in type inference.

and then call SumNodes(amount). Lots of options :)

Full example of the first form:

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

class Node
{
    public decimal Amount;
    public IEnumerable<Node> Children { get; set; }
}

public class Test
{
    static void Main()
    {
        var amounts = new Node {
            Amount = 10, Children = new[] {
                new Node { Amount = 20 },
                new Node { Amount = 30 }
            }
        };

        Func<Node, decimal> summer = null;
        summer = node => node.Amount + 
            (node.Children == null ? 0m : node.Children.Sum(summer));

        decimal total = summer(amounts);

        Console.WriteLine(total);
    }
}

I'm not sure I'd call any of these a "simple" LINQ query, mind you...

Jon Skeet
Jon, I'd upvote this a few more times, but SO won't let me.
John Saunders
Strictly, that is more of a "higher order function" than a true "recursive lambda"...
Marc Gravell
It wasn't right until I'd fixed it :)
Jon Skeet
Added a few more options...
Jon Skeet
I get an 'ambiguous invocation' error when I try to implement the SumNodes function.
Jan Aagaard
Ah yes, method group invocation oddities. Will fix.
Jon Skeet
Originally, I started writing it as an external method, and got the same method-group ambiguity - so I refactored using the delegate=null trick (same as your first one).
Marc Gravell
+2  A: 

Technically you can write recursive lambda expressions, but you need to be insane or insanely bright to try (I haven't figured out which). But you can cheat:

    Func<Node, decimal> nodeSum = null;
    nodeSum = node => {
        decimal result = node.Amount;
        if (node.Children != null) {
            result = result + node.Children.Sum(nodeSum);
        }
        return result;
    };
    var value = nodeSum(amounts);
Marc Gravell