Skip to content

Instantly share code, notes, and snippets.

@dmitry-pavlov
Created March 8, 2019 15:18
Show Gist options
  • Save dmitry-pavlov/f3933c937c3520a410ab15c3ebc24d5e to your computer and use it in GitHub Desktop.
Save dmitry-pavlov/f3933c937c3520a410ab15c3ebc24d5e to your computer and use it in GitHub Desktop.
Nice & universal way to convert List of items to Tree
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