I have a generic list of type Element, for example.
public class Element
{
public string Country { get; set; }
public string City { get; set; }
public int Population { get; set; }
}
With the following data.
var elements = new List<Element>
{
new Element { Country = "Country A", City = "Barrie", Population = 12 },
new Element { Country = "Country A", City = "Barrie2", Population = 12 },
new Element { Country = "Country A", City = "Barrie2", Population = 12 },
new Element { Country = "Country A", City = "Barrie", Population = 12 },
new Element { Country = "Country D", City = "Essex", Population = 12 },
new Element { Country = "Country A", City = "Barrie", Population = 12 },
new Element { Country = "Country A", City = "Barrie", Population = 12 },
new Element { Country = "Country D", City = "Essex", Population = 12 },
new Element { Country = "Country A", City = "Barrie", Population = 12 },
new Element { Country = "Country A", City = "Barrie", Population = 12 }
};
Essentially, I'd like a running total of the population grouped by country and city.
Something like.
Country A | Barrie | `running total for Barrie`
Country A | Barrie2 | `running total for Barrie2`
| | `total for Country A`
Country D | Essex | `running total for Essex`
| | `total for Country D`
| | `total for everything`
I couldn't find an extension (I say extension because I plan on using a rollup several times) anywhere so I figure I'd give it a shot. So I started with this simple query.
var groupedElements = elements
.GroupBy(x => new { x.Country, x.City })
.Select(x => new { Country = x.Key, City = x.Select(xx => xx.City), Population = x.Sum(xx => xx.Population) })
.ToList();
This query works as expected so I think I'm on the right track. Next I think I have to figure out which property from groupedElements is aggregate because that's what we'll be doing a rollup on. How do I accomplish that? Or perhaps I could have a parameter that makes me specify what column I wish to do the aggregate function on.
I don't think this is as easy to do as you might think. Firstly, the items of the result sequence you want are not 'naturally' of the same type. You want a running total within each country, grouped totals for each country, and then an overall total.
Even if you could write a terse query to create this information, the caller would have to differentiate between each kind of total from the result. I wonder if it is even makes much sense to view the result that you want as a single IEnumerable<XXX> in the first place; it might make much more sense to create a nice OO solution like this and proceed from there:
public interface IGeographicEnity
{
string Name { get; }
int Population { get; }
}
public class City : IGeographicEntity {...}
public class Country : IGeographicEntity
{
public IList<City> Cities { get {...} }
public int Population { get { return Cities.Sum(c => c.Population); } }
...
}
public class World : IGeographicEntity
{
public IList<Country> Countries { get {...} }
public int Population { get { return Countries.Sum(c => c.Population); }
...
}
If you still want to stick with your original idea, here's the best I can come up with:
public class PopulationTotal
{
// You can use subclassing instead of an enum to represent this
public enum Kind
{ RunningTotalWithinCountry, TotalForCountry, TotalOverall }
public string GroupName { get; private set; }
public int Value { get; private set; }
public Kind Kind { get; private set; }
public static IEnumerable<PopulationTotal> GetTotals(IEnumerable<Element> elements)
{
int overallTotal = 0;
foreach (var elementsByCountry in elements.GroupBy(e => e.Country))
{
int runningTotalForCountry = 0;
foreach (var element in elementsByCountry)
{
runningTotalForCountry += element.Population;
yield return new PopulationTotal
{
GroupName = element.City,
Kind = Kind.RunningTotalWithinCountry,
Value = runningTotalForCountry
};
}
overallTotal += runningTotalForCountry;
yield return new PopulationTotal
{
GroupName = elementsByCountry.Key,
Kind = Kind.TotalForCountry,
Value = runningTotalForCountry
};
}
yield return new PopulationTotal
{
GroupName = null,
Kind = Kind.TotalOverall,
Value = overallTotal
};
}
}
Usage:
var totals = PopulationTotal.GetTotals(elements);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With