derodevil
derodevil

Reputation: 1069

How to multiply and sum all numeric children properties in LINQ

How to multiply and sum all numeric children properties in LINQ

I have an object as follows

public class ResourceTier
{
    //primary key. no duplicate
    public int Id { get; set; }
    public int ParentId { get; set; }
    public decimal Volume { get; set; } //has value
    public decimal UnitRate { get; set; } //has value
    public decimal TotalPrice { get; set; } //has no value
}

The TotalPrice default value is 0. This is the property where I want to fill the value. The TotalPrice should be filled by multiplying the UnitRate and Volume properties and then summing of all the children if any. This is the data

| Id | ParentId | Volume | UnitRate   | TotalPrice  |
| 1  | 0        | -      | -          | 180         |
| 2  | 0        | -      | -          | 30          |
| 3  | 1        | -      | -          | 130         |
| 4  | 1        | 5      | 10         | 50          |
| 5  | 2        | 3      | 10         | 30          |
| 6  | 3        | -      | -          | 50          |
| 7  | 3        | 2      | 40         | 80          |
| 8  | 6        | 4      | 10         | 40          |
| 9  | 6        | 1      | 10         | 10          |

When I try this, it is not working. This code has only the sum of the direct children but not all children (grand grand children and so on)

List<ResourceTier> result = ...;
result.ForEach(x => x.TotalPrice =
        result.Where(c => c.ParentId == x.Id).Count() == 0 ?
        s.UnitRates * s.Volume :
        result.Where(c => c.ParentId == x.Id).Select(s => (s.UnitRates * s.Volume)).Sum());

Upvotes: 1

Views: 1450

Answers (3)

Harald Coppoolse
Harald Coppoolse

Reputation: 30512

LINQ can't change your source collection!

However you can create a new collection of ResourceTiers that match your requirements. After that you can decide to assign that to your output, or decide to save them in your database, table, etc.

IEnumerable<ResourceTier> sourceCollection = ...
IEnumerable<ResourceTier> resourceTiersWithTotalPrice = sourceCollection
    .Select(resourceTier => new ResourceTier
    {
        Id = resourceTier.Id,
        ParentId = resourceTier.ParentId,
        ...
        TotalPrice = resourceTier.Volume * resourceTier.UnitRate,
    });

Upvotes: -1

Markus
Markus

Reputation: 22511

A solution using only Linq will be hard to achieve, because it will typically only respect one level of children. In order to get the total of the grandchildren too, you have to walk the tree completely in order to get the sum of the children (and their children).

You have to create a method that uses recursion which means that it will call itself again with a different set of parameters. This way, you can get the total of the children first and then assign the value of the current node, e.g.:

private decimal SetTotal(IEnumerable<ResourceTier> tiers, ResourceTier current)
{
  current.Total = current.Volume * current.UnitRate;
  // Get children of current node
  var children = tiers.Where(x => x.ParentId == current.Id && x.Id != current.Id);  // The second condition explicitely excludes the current node to avoid infinite loops
  foreach(var child in children)
    current.Total += SetTotal(tiers, child);  // Call method again for children
  return current.Total;
} 

The first call to the function would use the ids of all items that do not have a parent, e.g.:

foreach(var topLevelNode in tiers.Where(x => x.ParentId == 0))
  SetTotal(tiers, topLevelNode);

Please note that the code above should demonstrate the principle of recursion. For sure there are more efficient ways to solve this a bit faster.

Upvotes: 3

Mehrdad Dowlatabadi
Mehrdad Dowlatabadi

Reputation: 1335

As other answers you need to calculate totalprice recursively:

 public static decimal total(ResourceTier rt, List<ResourceTier> data)
    {
        if (data.Where(x=>x.ParentId==rt.Id).Count()==0)
        return rt.Volume*rt.UnitRate;
        else
        {
          var sum=  data.Where(x => x.ParentId == rt.Id).Select(x=>total( x,data)).Sum();
            return rt.UnitRate*rt.Volume+sum;
        }
    }

And the use this method:

var data = new List<ResourceTier> {
    new ResourceTier{ Id=1, ParentId=0,  Volume=0, UnitRate=0, TotalPrice=0    },
    new ResourceTier{ Id=2, ParentId=0,  Volume=0, UnitRate=0, TotalPrice=0    },
    new ResourceTier{ Id=3, ParentId=1,  Volume=0, UnitRate=0, TotalPrice=0    },
    new ResourceTier{ Id=4, ParentId=1,  Volume=5, UnitRate=10, TotalPrice=0    },
    new ResourceTier{ Id=5, ParentId=2,  Volume=3, UnitRate=10, TotalPrice=0    },
    new ResourceTier{ Id=6, ParentId=3,  Volume=0, UnitRate=0, TotalPrice=0    },
    new ResourceTier{ Id=7, ParentId=3,  Volume=2, UnitRate=40, TotalPrice=0    },
    new ResourceTier{ Id=8, ParentId=6,  Volume=4, UnitRate=10, TotalPrice=0    },
    new ResourceTier{ Id=9, ParentId=6,  Volume=1, UnitRate=10, TotalPrice=0    },
};
var result = data.Select(x=>new ResourceTier { Id=x.Id, ParentId=x.ParentId, UnitRate=x.UnitRate, Volume=x.Volume, TotalPrice= total(x, data) }).ToList();

output :

Upvotes: 0

Related Questions