Created
March 8, 2019 15:18
-
-
Save dmitry-pavlov/f3933c937c3520a410ab15c3ebc24d5e to your computer and use it in GitHub Desktop.
Nice & universal way to convert List of items to Tree
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using Xunit; | |
using Xunit.Abstractions; | |
namespace ListToTree | |
{ | |
public class Category | |
{ | |
public int Id { get; set; } | |
public string Name { get; set; } | |
public int? ParentId { get; set; } | |
} | |
public interface ITree<T> | |
{ | |
T Data { get; } | |
ITree<T> Parent { get; } | |
ICollection<ITree<T>> Children { get; } | |
bool IsRoot { get; } | |
bool IsLeaf { get; } | |
int Level { get; } | |
} | |
public static class TreeExtensions | |
{ | |
/// <summary> Converts given collection to tree. </summary> | |
/// <typeparam name="T">Custom data type to associate with tree node.</typeparam> | |
/// <param name="items">The collection items.</param> | |
/// <param name="parentSelector">Expression to select parent.</param> | |
public static ITree<T> ToTree<T>(this IList<T> items, Func<T, T, bool> parentSelector) | |
{ | |
if (items == null) throw new ArgumentNullException(nameof(items)); | |
var lookup = items.ToLookup(item => items.FirstOrDefault(parent => parentSelector(parent, item)), | |
child => child); | |
return Tree<T>.FromLookup(lookup); | |
} | |
/// <summary> Internal implementation of <see cref="ITree{T}" /></summary> | |
/// <typeparam name="T">Custom data type to associate with tree node.</typeparam> | |
internal class Tree<T> : ITree<T> | |
{ | |
public T Data { get; } | |
public ITree<T> Parent { get; private set; } | |
public ICollection<ITree<T>> Children { get; } | |
public bool IsRoot => Parent == null; | |
public bool IsLeaf => Children.Count == 0; | |
public int Level => IsRoot ? 0 : Parent.Level + 1; | |
private Tree(T data) | |
{ | |
Children = new LinkedList<ITree<T>>(); | |
Data = data; | |
} | |
public static Tree<T> FromLookup(ILookup<T, T> lookup) | |
{ | |
var rootData = lookup.Count == 1 ? lookup.First().Key : default(T); | |
var root = new Tree<T>(rootData); | |
root.LoadChildren(lookup); | |
return root; | |
} | |
private void LoadChildren(ILookup<T, T> lookup) | |
{ | |
foreach (var data in lookup[Data]) | |
{ | |
var child = new Tree<T>(data) {Parent = this}; | |
Children.Add(child); | |
child.LoadChildren(lookup); | |
} | |
} | |
} | |
} | |
public class Tests | |
{ | |
private readonly ITestOutputHelper _output; | |
public Tests(ITestOutputHelper output) | |
{ | |
_output = output; | |
} | |
[Fact] | |
public void ToTreeTest() | |
{ | |
var categories = new List<Category>() | |
{ | |
new Category { Id = 1 , Name = "Sports" , ParentId = 0 }, | |
new Category { Id = 2 , Name = "Balls" , ParentId = 1 }, | |
new Category { Id = 3 , Name = "Shoes" , ParentId = 1 }, | |
new Category { Id = 4 , Name = "Electronics" , ParentId = 0 }, | |
new Category { Id = 5 , Name = "Cameras" , ParentId = 4 }, | |
new Category { Id = 6 , Name = "Lenses" , ParentId = 5 }, | |
new Category { Id = 7 , Name = "Tripod" , ParentId = 5 }, | |
new Category { Id = 8 , Name = "Computers" , ParentId = 4 }, | |
new Category { Id = 9 , Name = "Laptops" , ParentId = 8 }, | |
new Category { Id = 10 , Name = "Empty" , ParentId = 0 }, | |
new Category { Id = -1 , Name = "Broken" , ParentId = 999 } | |
}; | |
ITree<Category> tree = categories.ToTree((parent, child) => child.ParentId == parent.Id); | |
PrintNode(tree); | |
/* | |
<ROOT> | |
-Sports | |
--Balls | |
--Shoes | |
-Electronics | |
--Cameras | |
---Lenses | |
---Tripod | |
--Computers | |
---Laptops | |
-Empty | |
-Broken | |
*/ | |
Assert.True(tree.IsRoot); | |
} | |
private void PrintNode(ITree<Category> node) | |
{ | |
var indent = new string('-', node.Level); | |
var category = node.Data?.Name ?? "<ROOT>"; | |
_output.WriteLine($"{indent}{category}"); | |
foreach (var child in node.Children) | |
{ | |
PrintNode(child); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment