In order to be able to test my solution I've used in-memory data structures.
var dataSource = new List<LogModel>
{
new LogModel { Id = 1, Amount = 2, ProductName = "Fresh Apples", RecordDateTime = new DateTime(2021, 02, 23)},
new LogModel { Id = 2, Amount = 3, ProductName = "Sweet Bananas", RecordDateTime = new DateTime(2021, 06, 13)},
new LogModel { Id = 3, Amount = 1, ProductName = "Yellow Bananas", RecordDateTime = new DateTime(2021, 07, 12)},
new LogModel { Id = 4, Amount = 7, ProductName = "Green Apples", RecordDateTime = new DateTime(2021, 05, 31)},
new LogModel { Id = 5, Amount = 9, ProductName = "Juicy Apples", RecordDateTime = new DateTime(2021, 09, 12)},
new LogModel { Id = 6, Amount = 4, ProductName = "Young Potato's", RecordDateTime = new DateTime(2021, 02, 05)},
new LogModel { Id = 7, Amount = 5, ProductName = "Orange Carrots", RecordDateTime = new DateTime(2021, 06, 04)}
};
var categories = new List<CategoryModel>
{
new CategoryModel
{
ItemName = "Fruits",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Apples"},
new CategoryModel {ItemName = "Bananas"}
}
},
new CategoryModel
{
ItemName = "Vegetables",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Potato's"},
new CategoryModel {ItemName = "Carrots"}
}
}
};
The grouping logic can be written like this:
var topLevelQuantities = new Dictionary<string, int>();
foreach (var topLevelCategory in categories)
{
var filters = topLevelCategory.CategoryItems.Select(leafLevel => leafLevel.ItemName);
var count = dataSource.Where(log => filters.Any(filter => log.ProductName.Contains(filter)))
.Sum(log => log.Amount);
topLevelQuantities.Add(topLevelCategory.ItemName, count);
}
- Here I iterate through the top level categories
foreach (var topLevelCategory in categories)
- I assumed the the graph depth is 2 (so there are top level and leaf level entities)
- Then I gather the leafs'
ItemNames into the filters
- I perform a filtering based on the
filters dataSource.Where( ... filters.Any(...))
- And finally I calculate the
Sum of the filtered LogModels' Amount
- In order to be able to pass the accumulated data to another function/layer/whatever I've used a
Dictionary<string, int>
I've used the following command to examine the result:
foreach (var (categoryName, categoryQuantity) in topLevelQuantities.Select(item => (item.Key, item.Value)))
{
Console.WriteLine($"{categoryName}: {categoryQuantity}");
}
NOTES
- Please bear in mind that this solution was designed against in-memory data. So, after data has been fetched from the database. (If you want to perform this on the database level then that requires another type of solution.)
- Please also bear in mind that this solution requires multiple iterations over the
dataSource. So, if the dataSource is very large (or there are lots of top level categories) than the solution might not perform well.
UPDATE: When the depth of the category hierarchy is 3.
If we can assume that there can be only 1 top-level entity then we should not need to change too much:
var rootCategory = new CategoryModel
{
ItemName = "Categories",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel
{
ItemName = "Fruits",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Apples"},
new CategoryModel {ItemName = "Bananas"}
}
},
new CategoryModel
{
ItemName = "Vegetables",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Potato's"},
new CategoryModel {ItemName = "Carrots"}
}
}
}
};
var midLevelQuantities = new Dictionary<string, int>();
foreach (var midLevelCategory in rootCategory.CategoryItems)
{
...
}
If there can be multiple top level categories:
var categories = new List<CategoryModel>
{
new CategoryModel
{
ItemName = "Categories",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel
{
ItemName = "Fruits",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Apples"},
new CategoryModel {ItemName = "Bananas"}
}
},
new CategoryModel
{
ItemName = "Vegetables",
CategoryItems = new ObservableCollection<CategoryModel>
{
new CategoryModel {ItemName = "Potato's"},
new CategoryModel {ItemName = "Carrots"}
}
}
}
}
};
then we need to use recursive graph traversal.
I've introduced the following helper class to store the calculations' result:
public class Report
{
public string Name { get; }
public string Parent { get; }
public double Quantity { get; }
public Report(string name, string parent, double quantity)
{
Name = name;
Parent = parent;
Quantity = quantity;
}
}
The traversal can be implemented like this:
private static List<Report> GetReports(CategoryModel category, string parent, List<Report> summary)
{
if (category.CategoryItems == null || category.CategoryItems.Count == 0)
{
var count = dataSource.Where(log => log.ProductName.Contains(category.ItemName)).Sum(log => log.Amount);
summary.Add(new Report(category.ItemName, parent,count));
return summary;
}
foreach (var subCategory in category.CategoryItems)
{
summary = GetReports(subCategory, category.ItemName, summary);
}
var subTotal = summary.Where(s => s.Parent == category.ItemName).Sum(s => s.Quantity);
summary.Add(new Report(category.ItemName, parent, subTotal));
return summary;
}
- In the
if block we handle that case when we are at the leaf level
- That's where we perform the queries
- In the
foreach block we iterate through all of its children and we are calling the same function recursively
- After the
foreach loop we calculate the current category's subTotal by aggregating its children's Quantities
Please note: This design assumes that the category names are unique.
To display the results we need yet another recursive function:
private static void DisplayResult(int depth, IEnumerable<CategoryModel> categories, List<Report> report)
{
foreach (var category in categories)
{
var indentation = new string('\t', depth);
var data =report.Single(r => r.Name == category.ItemName);
Console.WriteLine($"{indentation}{data.Name}: {data.Quantity}");
if (category.CategoryItems == null || category.CategoryItems.Count == 0)
continue;
DisplayResult((byte)(depth +1), category.CategoryItems, report);
}
}
- Based on the category's level (
depth) we calculate the indentation
- We lookup the corresponding report based on the
ItemName property of the category
- If the current
category does not have subcategories then we move to the next item
- Otherwise we call the same function for the subcategories recursively
Let's put all things together
var report = new List<Report>();
foreach (var category in categories)
{
var subReport = GetReports(category, "-", new List<Report>());
report.AddRange(subReport);
}
DisplayResult(0, categories, report);
The output will be:
Categories: 31
Fruits: 22
Apples: 18
Bananas: 4
Vegetables: 9
Potato's: 4
Carrots: 5