I have a List of objects of type IGroup. These can be nested to an umlimited level, and I'm trying to group them after retrieving them from a database. I can't get my head around how to recursively add all groups to the right parents. Any groups with null as a parent are top level groups. I can't guarantee the order they come out of the database.
public interface IGroup {
string ID { get; set; }
string Name { get; set; }
string ParentID { get; set; }
IList<IGroup> Groups { get; set; }
...
So if I had a list of:
Group1: ID = g1, ParentID = null
Group1a: ID = g2, ParentID = g1
Group2: ID = g3, ParentID = null
Group1b: ID = g4, ParentID = g3
Group1bc: ID = g5, ParentID = g4
I'm trying to group them as:
|Group1
|--Group1a
|--Group1b
|--|
|--Group1bc
|Group2
Anyone fancy a stab at grouping them recursively?
No need to be recursive. To wit:
var lookup = items.ToDictionary(g => g.ID); // items is IEnumerable<IGroup>
foreach (var item in items.Where(g => g.ParentID != null)) {
lookup[item.ParentID].Groups.Add(item);
}
var parents = items.Where(g => g.ParentID == null);
Note that lookup[item.ParentID] will throw if there is no IGroup with the corresponding ParentID. You can handle this more gracefully with TryGetValue.
My implementation of IGroup:
public class Group : IGroup {
public string ID { get; set; }
public string Name { get; set; }
public string ParentID { get; set; }
public IList<IGroup> Groups { get; set; }
public Group() {
Groups = new List<IGroup>();
}
}
My test items:
IEnumerable<IGroup> items = new List<IGroup>() {
new Group() { ID = "g1", ParentID = null },
new Group() { ID = "g2", ParentID = "g1" },
new Group() { ID = "g3", ParentID = null },
new Group() { ID = "g4", ParentID = "g3" },
new Group() { ID = "g5", ParentID = "g4" },
new Group() { ID = "g6", ParentID = "g5" }
};
This is not recursive, but here's a solution (assuming you have all you groups in a list called groups)
var rootGroups = new List<IGroup>();
var dic = groups.ToDictionary(g => g.ID);
foreach (var g in groups)
{
if (g.ParentID == null)
{
rootGroups.Add(g);
}
else
{
IGroup parent;
if (dic.TryGetValue(g.ParentID, out parent))
{
parent.Groups.Add(g);
}
}
}
You could try ordering them by parent id, assuming that the parent group is always created before the child group.
Group by ParentID (Linq: GroupBy), order by ID.
Start with an empty root node (ID: null) and add all items with this ParentID. Recursively continue this process for any item that has been added.
As you extract each element from the database, you need to add it to its parent. So keep a Dictionary to help find the parent. If you get a child before its parent, then you can insert a placeholder until you get the real thing.
void BuildGroups()
{
foreach( IGroup x /* obtained from database or a collection or wherever */ )
AddGroup( x );
}
Dictionary<string,IGroup> _groups = new Dictionary<string,IGroup>;
string _parentGroupName = "PARENT";
void AddGroup( IGroup x )
{
// locate (or create) parent and add incoming group
IGroup parent;
string parentID = x.ParentID ?? _parentGroupName;
if( !groups.TryGetValue( parentID, out parent ) )
{
parent = new Group( parentID ); // SEE NOTE BELOW!
_groups[parentID] = parent;
}
parent.Groups.Add( x );
// locate (or insert) child, and make sure it's up to date
IGroup child;
if( groups.TryGetValue( x.ID, out child ) )
{
// We must have inserted this ID before, because we found one
// of ITS children first. If there are any other values besides
// ParentID and ID, then copy them from X to child here.
}
else
{
// first time we've seen this child ID -- insert it
_groups[x.ID] = x;
}
}
The dictionary element at _parentGroupName will then be a dummy node whose children are all of your top-level groups (i.e. the ones with NULL as ParentID from the database); from that element you can do a recursive traversal:
VisitGroups( _groups[_parentGroupName], "" );
void VisitGroups( string ID, string indent )
{
IGroup x;
if( _groups.TryGetValue( ID, out x ) )
{
foreach( IGroup y in x.Groups )
{
WriteLine( indent + " {0}", y.ID );
VisitGroups( y.ID, indent + " " );
}
}
}
NOTE: This implementation runs in a single inline pass through the data -- you can add elements immediately as they're retrieved from the database, and you only need to make a single pass through the data. That means you save some time and some memory. But in return, it requires that you be able to allocate an object with type IGroup() to act as a placeholder, in case a child is retrieved before its parent. You can only avoid that requirement if you know something about the order of the objects or if you process your dictionary in two passes, as is shown in the other answers.
I use a sentinel value, _parentGroupName, to keep the top-level nodes in the same collection as all the others. You can easily alter this to use a separate collection for the top level nodes instead if you prefer.
You could try this
public interface IGroup
{
string ID { get; set; }
string Name { get; set; }
string ParentID { get; set; }
List<IGroup> Groups { get; set; }
}
public class Group : IGroup
{
public string ID { get; set; }
public string Name { get; set; }
public string ParentID { get; set; }
public List<IGroup> Groups { get; set; }
public Group()
{
}
public Group(string id, string name, List<IGroup> childs)
{
ID = id;
Name = name;
Groups = (List<IGroup>)childs.Cast<IGroup>();
}
}
class Program
{
static void Main(string[] args)
{
List<IGroup> OriginalList;
List<IGroup> HirarchList = new List<IGroup>();
OriginalList = new List<IGroup>()
{
new Group() { ID = "g1", ParentID = null },
new Group() { ID = "g2", ParentID = "g1" },
new Group() { ID = "g3", ParentID = null },
new Group() { ID = "g4", ParentID = "g3" },
new Group() { ID = "g5", ParentID = "g4" },
new Group() { ID = "g6", ParentID = "g5" } };
HirarchList = GetCreateList(null, OriginalList);
}
public static List<IGroup> GetCreateList(string id, List<IGroup> list)
{
List<IGroup> temp = new List<IGroup>();
temp = (from item in list
where item.ParentID == id
select (IGroup)new Group(item.ID, item.Name,GetCreateList(item.ID, list))).ToList();
return (List<IGroup>)temp;
}
}
Related
This question already has answers here:
How to flatten tree via LINQ?
(15 answers)
Closed 6 months ago.
I've the following class, which contains some info about the object it also has a list of same object and hierarchy goes on. This is my class:
public class Category
{
public List<Category>? children { get; set; }
public bool var { get; set; }
public string? name { get; set; }
public bool leaf { get; set; }
public int category_id { get; set; }
}
I have a list List<Category> categories; I want to loop over the list and go deep down in every children and create this new object:
public class DBCategory
{
public string? CategoryId { get; set; }
public string? CategoryName { get; set; }
public string? CategoryParentId { get; set; }
}
I have tried to loop over my list and then call function recursively but I'm also stuck there because children isn't a category class but a list of categories so the function fails to accept parameter in if clause:
foreach (var category in categories)
{
CreateDBCategory(category);
}
DBCategory CreateDBCategory(Category category)
{
DBCategory dBCategory = new DBCategory();
if (category.children.Count > 0)
{
return CreateDBCategory(category.children);
}
return dBCategory;
}
I have also tried to reach most bottom child by this, but this code says not all paths return a value.
DBCategory testFunction(List<Category> categories)
{
foreach (var category in categories)
{
if (category.children.Count > 0)
{
return testFunction(category.children);
}
else
{
return category;
}
}
}
One of the common ways to handle such cases is to have the List to be filled passed as an argument to the method. E.g.:
List<DBCategory> dbCategories = new();
foreach (var category in categories)
{
CreateDBCategory(category, dbCategories);
}
void CreateDBCategory(Category category, List<DBCategory> dbCategories)
{
DBCategory dbCategory = new DBCategory();
// Fill dbCategory
dBCategories.Add(dbCategory);
if (category.children != null)
{
// recurse over all children categories and add them to the list
foreach (var child in category.children)
{
CreateDBCategory(child, dbCategories);
}
}
}
It could be argued that this solution does not fit the functional paradigm as it has side effects (modifying the passed in List), so an alternative, more functional approach would be to return a list from the recursive method, e.g.:
List<DBCategory> dbCategories = new();
foreach (var category in categories)
{
dbCategories.AddRange(CreateDBCategory(category));
}
IEnumerable<DBCategory> CreateDBCategory(Category category)
{
List<DBCategory> dbCategories = new();
DBCategory dbCategory = new DBCategory();
// Fill dbCategory
dbCategories.Add(dbCategory);
if (category.children != null)
{
// recurse over all children categories and add them to the list
foreach (var child in category.children)
{
dbCategories.AddRange(CreateDBCategory(child));
}
}
return dbCategories;
}
This does however perform a lot more allocations, so in some cases it can perform slower than the first approach
Noted that this is untested, but it should work.
IEnumerable<DBCategory> FlattenCategories(IEnumerable<Category> categories, int parentId)
{
DBCategory selector(Category cat, int pid) =>
return categories
.Select(c => new DBCategory {
CategoryId = cat.category_id,
CategoryName = cat.name,
CategoryParentId = pid,
})
.Concat(categories.SelectMany(
c => FlattenCategories(c.children, c.category_id)
);
}
Just call FlattenCategories(categories).ToList(); to get List<DBCategory>
From here, A generic solution.
public static IEnumerable<T> Traverse<T>(
this T root,
Func<T, IEnumerable<T>> childrenSelector)
{
ArgumentNullException.ThrowIfNull(childrenSelector);
var stack = new Stack<T>();
stack.Push(root);
while(stack.Count > 0)
{
var current = stack.Pop();
yield return current;
foreach(var child in childrenSelector(current))
{
stack.Push(child);
}
}
}
So you can do this,
foreach(var category in root.Traverse(c => c.Children))
{
...
}
or some LINQ. The beauty is, it won't allocate more memory than your biggest leaf collection and won't have a stack overflow for deep trees.
Try following :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication40
{
class Program
{
static void Main(string[] args)
{
Category root = new Category()
{
children = new List<Category>() {
new Category() {
children = new List<Category>() {
new Category() {
var = true,
name = "2A",
leaf = true,
category_id = 21
},
new Category() {
var = true,
name = "2B",
leaf = true,
category_id = 22
}
},
var = true,
name = "1A",
leaf = false,
category_id = 1
},
new Category() {
children = new List<Category>() {
new Category() {
var = true,
name = "2C",
leaf = true,
category_id = 23
},
new Category() {
var = true,
name = "2D",
leaf = true,
category_id = 24
}
},
var = true,
name = "1B",
leaf = false,
category_id = 2
},
},
category_id = 0,
name = "root",
leaf = false,
var = true
};
List<DBCategory> children = DBCategory.GetChildren(root,null);
}
}
public class Category
{
public List<Category> children { get; set; }
public bool var { get; set; }
public string name { get; set; }
public bool leaf { get; set; }
public int category_id { get; set; }
}
public class DBCategory
{
public int? CategoryId { get; set; }
public string CategoryName { get; set; }
public int? CategoryParentId { get; set; }
public static List<DBCategory> GetChildren(Category catgory, int? parentId)
{
List<DBCategory> results = new List<DBCategory>() { new DBCategory() {
CategoryId = catgory.category_id,
CategoryName = catgory.name,
CategoryParentId = parentId
}};
if (catgory.children != null)
{
foreach (Category child in catgory.children)
{
results.AddRange(GetChildren(child, catgory.category_id));
}
}
return results;
}
}
}
I have a very simple test project where I am trying to figure out how to add children into the parent's collection.
The datamodel is quite basic:
Current result returns a duplicated entry.
Expected/desired
I expected the result to be just one entry with two children
GROUP1 -> { USER_1, USER_2 }
GROUP class
public class GROUP
{
public GROUP()
{
this.USERs = new HashSet<USER>();
}
public int Group_ID { get; set; }
public string Name { get; set; }
public string Location { get; set; }
public ICollection<USER> USERs { get; set; }
}
USER class
public class USER
{
public int User_ID { get; set; }
public int Group_ID { get; set; }
public string Name { get; set; }
public Nullable<int> Age { get; set; }
public GROUP GROUP { get; set; }
}
Dapper method
public GROUP Get(int id)
{
string sqlGetGroupExtended = $"SELECT _group.Group_ID, _group.Name, _group.Location, _user.User_ID, _user.Name, _user.GROUP_ID, _user.Age FROM dbo.[GROUP] _group " +
"LEFT JOIN dbo.[USER] _user ON _group.Group_ID = _user.Group_ID " +
"WHERE _group.Group_ID = #groupid;";
GROUP result = null;
var lookup = new Dictionary<int, GROUP>();
using (var connection = new SqlConnection(Properties.Settings.Default.CodeTest_DB))
{
var extendedGroup = connection.Query<GROUP, USER, GROUP>(sqlGetGroupExtended, (parent, child) =>
{
if (!lookup.TryGetValue(parent.Group_ID, out GROUP found))
{
lookup.Add(parent.Group_ID, found = parent);
}
found.USERs.Add(child);
return found;
}, param: new { groupid = id }, splitOn: "Location");
// result = extendedGroup <--- WHAT TO DO HERE?
}
return result;
}
How can I achieve this?
References:
http://dapper-tutorial.net/dapper
https://github.com/StackExchange/Dapper/blob/master/Dapper.Tests/MultiMapTests.cs#L12
If you're using SQL Server 2016 or Azure SQL you can take advance of JSON to return a hierarchical object:
https://medium.com/dapper-net/one-to-many-mapping-with-dapper-55ae6a65cfd4
It's an article I wrote on the subject, along with source code, that shows how to elegantly solve the problem.
My bad, as the code shows here https://github.com/StackExchange/Dapper/blob/master/Dapper.Tests/MultiMapTests.cs#L12
I was missing the .Distinct()
var extendedGroup = connection.Query<GROUP, USER, GROUP>(sqlGetGroupExtended, (parent, child) =>
{
if (!lookup.TryGetValue(parent.Group_ID, out GROUP found))
{
lookup.Add(parent.Group_ID, found = parent);
}
found.USERs.Add(child);
return found;
}, param: new { groupid = id }, splitOn: "Location,User_ID").Distinct();
I have a list and I need to sort it to this hierarchy
{ Id=1, ParentId = null, Name = "Item1", Type="0"}
{ Id=2, ParentId = 1, Name = "ItemChild1", Type="1"}
{ Id=3, ParentId = 1, Name = "ItemChild2", Type="1"}
{ Id=4, ParentId = 3, Name = "ItemGrandChild1", Type="2"}
{ Id=5, **ParentId = 1**, Name = "ItemGrandChild2", Type="2"}
{ Id=6, ParentId = null, Name = "Item7", Type="0"}
...
Unlike normal parent child relationships, here
Items of Type2 can be child of either Type1 or Type0
All the Id's are guids
I have seen may stack answers on child parent sorting using Linq. But my case is different.
Any elegant way using Linq ?
If I were you, I would give up trying to sort this with either linq or sql. Abstractions can be helpful, but in a complex case like this they will just get in your way.
You will save a lot of time if you just write your own sorter.
class MyCompare : IComparer<MyObject>
{
public int Compare(x1, x2)
{
if (x1.parent == parent1 && x2.parent == parent2)
return 1;
if (x1.parent == x2.parent)
return 0;
//ect
}
}
List<MyObject> list = GetWeirdObjects();
list.Sort(new MyCompare());
Create a lookup to find fast the children and project them to the parent collection. As far as i see it does not depend at all if your children have different types as long as they know which element is their parent.
public class TreeItem
{
public int Id { get; set; }
public int? ParentId { get; set; }
public IEnumerable<TreeItem> Children { get; set; }
public void PrintAllChildren()
{
this.PrintAllChildren(0);
}
private void PrintAllChildren(int indent)
{
Debug.WriteLine("{1}Item id: {0}", this.Id, string.Concat(Enumerable.Repeat<int>(0, indent).Select(i => " ")));
if (this.Children != null)
foreach (var item in this.Children)
item.PrintAllChildren(indent + 1);
}
}
public static class TreeItemExtension
{
public static IEnumerable<TreeItem> GetAsTree(this IEnumerable<TreeItem> data)
{
var lookup = data.ToLookup(i => i.ParentId);
return lookup[null].Select(i => {
i.FillChildren(lookup);
return i;
});
}
private static TreeItem FillChildren(this TreeItem item, ILookup<int?, TreeItem> lookup)
{
item.Children = lookup[item.Id].Select(i => i.FillChildren(lookup));
return item;
}
}
I'm having trouble conceptualizing something that should be fairly simple using LINQ. I have a collection that I want to narrow down, or filter, based on the id values of child objects.
My primary collection consists of a List of Spots. This is what a spot looks like:
public class Spot
{
public virtual int? ID { get; set; }
public virtual string Name { get; set; }
public virtual string Description { get; set; }
public virtual string TheGood { get; set; }
public virtual string TheBad { get; set; }
public virtual IEnumerable<Season> Seasons { get; set; }
public virtual IEnumerable<PhotographyType> PhotographyTypes { get; set; }
}
I'm trying to filter the list of Spots by PhotographyType and Season. I have a list of ids for PhotographyTypes and Seasons, each in an int[] array. Those lists look like this:
criteria.PhotographyTypeIds //an int[]
criteria.SeasonIds //an int[]
I want to build a collection that only contains Spots with child objects (ids) matching those in the above lists. The goal of this functionality is filtering a set of photography spots by type and season and only displaying those that match. Any suggestions would be greatly appreciated.
Thanks everyone for the suggestions. I ended up solving the problem. It's not the best way I'm sure but it's working now. Because this is a search filter, there are a lot of conditions.
private List<Spot> FilterSpots(List<Spot> spots, SearchCriteriaModel criteria)
{
if (criteria.PhotographyTypeIds != null || criteria.SeasonIds != null)
{
List<Spot> filteredSpots = new List<Spot>();
if (criteria.PhotographyTypeIds != null)
{
foreach (int id in criteria.PhotographyTypeIds)
{
var matchingSpots = spots.Where(x => x.PhotographyTypes.Any(p => p.ID == id));
filteredSpots.AddRange(matchingSpots.ToList());
}
}
if (criteria.SeasonIds != null)
{
foreach (int id in criteria.SeasonIds)
{
if (filteredSpots.Count() > 0)
{
filteredSpots = filteredSpots.Where(x => x.Seasons.Any(p => p.ID == id)).ToList();
}
else
{
var matchingSpots = spots.Where(x => x.Seasons.Any(p => p.ID == id));
filteredSpots.AddRange(matchingSpots.ToList());
}
}
}
return filteredSpots;
}
else
{
return spots;
}
}
You have an array of IDs that has a Contains extension method that will return true when the ID is in the list. Combined with LINQ Where you'll get:
List<Spot> spots; // List of spots
int[] seasonIDs; // List of season IDs
var seasonSpots = from s in spots
where s.ID != null
where seasonIDs.Contains((int)s.ID)
select s;
You can then convert the returned IEnumerable<Spot> into a list if you want:
var seasonSpotsList = seasonSpots.ToList();
This may helps you:
List<Spot> spots = new List<Spot>();
Spot s1 = new Spot();
s1.Seasons = new List<Season>()
{ new Season() { ID = 1 },
new Season() { ID = 2 },
new Season() { ID = 3 }
};
s1.PhotographyTypes = new List<PhotographyType>()
{ new PhotographyType() { ID = 1 },
new PhotographyType() { ID = 2 }
};
Spot s2 = new Spot();
s2.Seasons = new List<Season>()
{ new Season() { ID = 3 },
new Season() { ID = 4 },
new Season() { ID = 5 }
};
s2.PhotographyTypes = new List<PhotographyType>()
{ new PhotographyType() { ID = 2 },
new PhotographyType() { ID = 3 }
};
List<int> PhotographyTypeIds = new List<int>() { 1, 2};
List<int> SeasonIds = new List<int>() { 1, 2, 3, 4 };
spots.Add(s1);
spots.Add(s2);
Then:
var result = spots
.Where(input => input.Seasons.All
(i => SeasonIds.Contains(i.ID))
&& input.PhotographyTypes.All
(j => PhotographyTypeIds.Contains(j.ID))
).ToList();
// it will return 1 value
Assuming:
public class Season
{
public int ID { get; set; }
//some codes
}
public class PhotographyType
{
public int ID { get; set; }
//some codes
}
I have designed a Class for Parent Child relationship
class Category
{
public string CatName;
public string CatId;
public IList<Category> childCategory = new List<Category>();
public void addChildCat(Category childCat)
{
this.childCategory.Add(childCat);
}
public Category SortedCategory(Category cat)
{
// Should return the sorted cat i.e topmost parent
}
}
Here by Parent will not have Catname or CatId, it will have Child Categories which has CatName, CatId as well as another Child Category List and it goes till "N" categories
Here I need to get the Top Parent with all the child categories sorted by CatName. Any ideas How this can be achieved?
Is my class design GOOD?
You can't because you have not a reference to the parent. You have to add a field:
public Category Parent { get; set; }
and modify the add method to set the parent:
public void addChildCat(Category childCat)
{
childCat.Parent = this;
this.childCategory.Add(childCat);
}
You need the parent to get the root:
public static Category SortedCategory(Category cat)
{
// get the root
var root = cat;
while(root.Parent != null) root = root.Parent;
return root.GetSorted();
}
private Category GetSorted()
{
var sortedChildren = new List<Category>(childCategories).ConvertAll(c => c.GetSorted());
sortedChildren.Sort((c1, c2) => c1.CatName.CompareTo(c2.Catname));
return new Category { CatName = root.CatName,
Catid = root.CatId,
childCategories = sortedChildren; }
}
You can use the SortedList to keep track of the child categories instead.
If I understand this, we have a tree structure right? And what is the result you are expecting, the sorted children of the topmost parent (root)?
Instead of :
public string CatName;
public string CatId;
I would do:
class Cat
{
public string Name { get; set; }
public string Id { get; set; }
}
And instead of:
public Category SortedCategory(Category cat)
{
// Should return the sorted cat i.e topmost parent
}
I would do:
var category = new List<Cat>
{
new Cat() {Name = "cat1", Id = "123"},
new Cat() {Name = "cat2", Id = "124"},
new Cat() {Name = "cat3", Id = "125"},
new Cat() {Name = "cat4", Id = "126"}
};
category.Sort(( cat1, cat2) => ((Convert.ToInt32(cat1.Id) > Convert.ToInt32(cat2.Id)) ? 1 : 0) );