How to group only subsequent items with the same property using linq - c#

I have input that could look like this:
A 1 2 C,D
A 2 3 C,E
B 4 5 F
A 6 7
A 7 8 D
A 9 10 E
I store this in my model class:
public class Item {
public String Name {get;set;}
public int Start {get;set;}
public int End {get;set;}
public List<string> Orders {get;set;}
}
I tried to use Linq to merge all subsequent items if the items have the same name and generate a new item that has the start value of the first item in the group, the end value of the last item in the group and a union of all order lists. It should then look like this:
A 1 3 C,D,E
B 4 5 F
A 6 10 D, E
I tried the following Linq statement, however, it groups all As and Bs together, independent of whether there are any other items in between. What do I need to change? The union of the order list is also missing.
var groups = items.GroupBy(i => i.Name).ToList();
foreach (var group in groups)
{
result.Add(new Item {
Start = group.First().Start,
End = group.Last().End,
Name = group.First().Name });
}

Use a classic loop for this:
var List<List<Item>> groups = new List<List<Item>>()
var currentGroup = new List<Item> { items.First() };
int i = 0;
foreach(var item in items.Skip(1))
{
if(currentGroup.First().Name != item.Name)
{
groups.Add(currentGroup);
currentGroup = new List<Item> { item };
}
else
{
currentGroup.Add(item);
if(i == items.Count - 2)
groups.Add(currentGroup);
}
i++;
}
Now you can continue with your code by iterating the groups-list.

Maybe not the best or fastest way but I got bored:
int groupID = -1;
var result = items.Select((item, index) =>
{
if (index == 0 || items[index - 1].Name != item.Name)
++groupID;
return new { group = groupID, item = item };
}).GroupBy(item => item.group).Select(group =>
{
Item item = new Item();
var first = group.First().item;
var last = group.Last().item;
item.Name = first.Name;
item.Start = first.Start;
item.End = last.End;
item.Orders = group.SelectMany(g => g.item.Orders).Distinct().ToList();
return item;
});
The variable items should be your input collection like a List<Item>. The result will be stored in result. This is an IEnumerable<Item> but you may add .ToList() or .ToArray() as you like to convert it to List<Item> or Item[].
The result will contain new created items. I did this on purpose to not mess up the input data.
The trick here is to use a local variable as a group id. It is increased if it is the first item or the last item had a different name. Then we group by the group id and the rest of the code will just create the item. The SelectMany method will join all Orders-values from the entire group and Distinct will then remove all duplicates.

This is not done by Linq. I just played a bit using simpler methods. But it gives same result which you wanted.
using System;
using System.Collections.Generic;
public class Item
{
public static List<Item> Database;
static Item()
{
Database = new List<Item>();
}
public Item(string name, int start, int end, params string[] orders)
{
Name = name;
Start = start;
End = end;
Orders = new List<string>();
foreach (string s in orders)
Orders.Add(s);
//putting newly created Item to database
Database.Add(this);
}
//overload for creating tmp Items in GroupThem(), could be done using optinional parameter
public Item(bool AddToDatabase, string name, int start, int end, params string[] orders)
{
Name = name;
Start = start;
End = end;
Orders = new List<string>();
foreach (string s in orders)
Orders.Add(s);
if (AddToDatabase) Database.Add(this);
}
public string Name { get; set; }
public int Start { get; set; }
public int End { get; set; }
public List<string> Orders { get; set; }
public List<Item> GroupedItems()
{
List<Item> groupedItems = new List<Item>();
Item previous = Database[0];
Stack<Item> sameItems = new Stack<Item>();
foreach (Item item in Database)
{
if (previous.Name == item.Name)
{
sameItems.Push(item);
}
else
{
groupedItems.Add(GroupThem(sameItems));
previous = item;
sameItems.Push(item);
}
}
groupedItems.Add(GroupThem(sameItems));
return groupedItems;
}
private Item GroupThem(Stack<Item> sameItems)
{
string newName = "";
int newEnd = 0;
int newStart = int.MaxValue;
List<string> newOrders = new List<string>();
Item tmp = null;
while (sameItems.Count > 0)
{
tmp = sameItems.Pop();
if (tmp.Start < newStart)
newStart = tmp.Start;
if (tmp.End > newEnd)
newEnd = tmp.End;
foreach (string s in tmp.Orders)
if (!newOrders.Contains(s))
newOrders.Add(s);
newName = tmp.Name;
}
return new Item(false, newName, newStart, newEnd, newOrders.ToArray());
}
public override string ToString()
{
string tmp = "";
foreach (string s in Orders)
tmp += " " + s;
return "Name = " + Name + ", Start = " + Start + ", End = " + End +", Orders = "+ tmp;
}
}
class Program
{
static void Main(string[] args)
{
Item item1 = new Item("A", 1, 2, "C", "D");
Item item2 = new Item("A", 2, 3, "C", "E");
Item item3 = new Item("B", 4, 5, "F");
Item item4 = new Item("A", 6, 7);
Item item5 = new Item("A", 7, 8, "D");
Item item6 = new Item("A", 9, 10, "E");
foreach (Item item in item1.GroupedItems())
{
Console.WriteLine(item);
}
}
}

A bit late, I know, but I think the following solution will still help someone.
It includes the original Item class, enhanced with:
A ToString method to simplify inspection.
A CreateSamples method to generate the sample items.
A bonus nested class ComparerByStartAndEnd to sort items based on Start and End properties.
The solution resides in the EXTENSIONS.GroupWhenChanging method and the Item.FromGroup method.
The TEST class provides code to verify everything works as expected.
The actual grouping logic (EXTENSIONS.GroupWhenChanging) simply implements an enumerator that does not invoke Linq methods and allocates only a List object for each group, thus saving both in performance and memory resources.
The method is generic and accepts a comparison predicate, so it is not restricted to the sample Item class.
The creation of the result items, representing the groups with merged orders, is kept in the separate method Item.FromGroup. It uses some Linq to ease the task.
The TEST.Test method does the following:
Creates the list of samples.
Ensures the samples are ordered based on Start and End.
Enumerates the groups (by means of GroupWhenChanging) and creates the corresponing items (through Item.FromGroup).
The Item class:
public static class MODEL
{
public class Item
{
public String Name { get; set; }
public int Start { get; set; }
public int End { get; set; }
public List<string> Orders { get; set; }
/// <inheritdoc/>
public override string ToString()
{
return string.Format("{0} {1} .. {2} {3}", this.Name, this.Start, this.End, string.Join(",", this.Orders));
}
public static Item? FromGroup(IEnumerable<Item> group)
{
var array = group as Item[] ?? group.ToArray();
if (array.Length > 0)
{
var newName = array[0].Name;
var newStart = array.Min(item => item.Start);
var newEnd = array.Max(item => item.End);
var newOrders = array.SelectMany(item => item.Orders).Distinct().OrderBy(orderID => orderID).ToList();
var newItem = new Item()
{
Name = newName,
Start = newStart,
End = newEnd,
Orders = newOrders
};
return newItem;
}
return null;
}
public static IEnumerable<Item> CreateSamples()
{
yield return new Item() { Name = "A", Start = 1, End = 2, Orders = new List<string>() { "C", "D" } };
yield return new Item() { Name = "A", Start = 2, End = 3, Orders = new List<string>() { "C", "E" } };
yield return new Item() { Name = "B", Start = 4, End = 5, Orders = new List<string>() { "F" } };
yield return new Item() { Name = "A", Start = 6, End = 7, Orders = new List<string>() };
yield return new Item() { Name = "A", Start = 7, End = 8, Orders = new List<string>() { "D" } };
yield return new Item() { Name = "A", Start = 9, End = 10, Orders = new List<string>() { "E" } };
}
public sealed class ComparerByStartAndEnd : Comparer<Item>
{
/// <inheritdoc/>
public override int Compare(Item x, Item y)
{
if (x == y)
return 0;
return x.End.CompareTo(y.Start);
}
}
}
}
The EXTENSIONS class:
public static class EXTENSIONS
{
public static IEnumerable<IEnumerable<T>> GroupWhenChanging<T>(this IEnumerable<T> items, Func<T, T, bool> predicate)
{
List<T> group = null;
foreach (var item in items)
{
if (group is null)
group = new List<T>() { item };
else if (predicate(group[group.Count - 1], item))
group.Add(item);
else
{
yield return group;
group = new List<T>() { item };
}
}
if (group is not null)
yield return group;
}
}
The TEST class:
public static class TEST
{
public static void Test()
{
var items = MODEL.Item.CreateSamples().ToList();
items.Sort(new MODEL.Item.ComparerByStartAndEnd());
var groups = items
.GroupWhenChanging((prev, next) => prev.Name == next.Name)
.Select(MODEL.Item.FromGroup)
.ToList();
}
}

Related

c# method with List / param int[] parameters

I have to make method called CalculateBill with given parameters, but I cannot define how to connect those two parameters inside of code.
public class Bill {
public int CalculateBill (List<Item> itemList, params int[] items) {
// Need to write code here
//itemList : available items in the shop
// int[] items : array of ItemId's purchased by customer
// A customer can buy same item many times , so itemId can be repeated in items array
// return sum of al items prices
}
}
The item class is like
public class Item {
public int ItemId {get; set;}
public int ItemPrice { get; set; }
}
The Program Class is like
List<Item> itemList = new List<Item> {
new Item() {ItemId = 100 , ItemPrice = 50},
new Item() {ItemId = 101 , ItemPrice = 60},
new Item() {ItemId = 102 , ItemPrice = 20},
};
Bill bObj = new Bill();
int a = bObj.CalculateBill(itemList, 101, 102, 101);
int b = bObj.CalculateBill(itemList);
It sounds like you need help implementing the CalculateBill function using your existing architecture. I would implement it like below.
I renamed your 'itemList' parameter to 'itemsInShop' and I also renamed your 'items' parameter to 'itemsPurchasedIDs'. Always try to make your variable names as descriptive as possible to make it easy to understand and read.
public int CalculateBill(List<Item> itemsInShop, params int[] itemsPurchasedIDs)
{
int totalSpent = 0;
for (int i = 0; i < itemsPurchasedIDs.Length; i++)
{
int itemId = itemsPurchasedIDs[i];
for (int j = 0; j < itemsInShop.Count; j++)
{
var shopItem = itemsInShop[j];
if (shopItem.ItemId == itemId)
{
totalSpent += shopItem.ItemPrice;
break;
}
}
}
return totalSpent;
}
You are almost there. Create a class for the shop that contains the items available so you don't need to pass it as an argument.
Also, since you want to retrieve Items with id values, you need a Dictionary<int,Item> which would allow you to do this quickly.
Here is the Shop class with a dictionary
public class Shop
{
private readonly Dictionary<int, Item> _items;
public Shop()
{
_items = new Dictionary<int, Item>();
}
public IReadOnlyList<Item> Items { get => _items.Values.ToList(); }
public void Add(Item item) => _items.Add(item.ItemId, item);
public Item[] GetItems(params int[] idList)
{
List<Item> list = new List<Item>();
foreach (var id in idList)
{
list.Add(_items[id]);
}
return list.ToArray();
}
public decimal CalculateBill(params int[] idList)
{
var items = GetItems(idList);
decimal sum = 0m;
foreach (var item in items)
{
sum += item.ItemPrice;
}
return sum;
}
}
Otherwise, you would need to lookup the items one by one. Here is a solution that uses theList.Find() method to match the Item from the id.
public class Shop
{
private readonly List<Item> _items;
public Shop()
{
_items = new List<Item>();
}
public IReadOnlyList<Item> Items { get => _items.ToList(); }
public void Add(Item item) => _items.Add(item);
public Item[] GetItems(params int[] idList)
{
List<Item> list = new List<Item>();
foreach (var id in idList)
{
list.Add(_items.Find((item)=>item.ItemId==id));
}
return list.ToArray();
}
public decimal CalculateBill(params int[] idList)
{
var items = GetItems(idList);
decimal sum = 0m;
foreach (var item in items)
{
sum += item.ItemPrice;
}
return sum;
}
}
Here are three alternatives for you to try:
public int CalculateBill(List<Item> itemsInShop, params int[] itemsPurchasedIDs) =>
itemsPurchasedIDs
.Join(itemsInShop, id => id, item => item.ItemId, (_, item) => item.ItemPrice)
.Sum();
public int CalculateBill2(List<Item> itemsInShop, params int[] itemsPurchasedIDs) =>
(
from id in itemsPurchasedIDs
join item in itemsInShop on id equals item.ItemId
select item.ItemPrice
).Sum();
public int CalculateBill3(List<Item> itemsInShop, params int[] itemsPurchasedIDs)
{
var map = itemsInShop.ToDictionary(x => x.ItemId, x => x.ItemPrice);
return itemsPurchasedIDs.Sum(x => map[x]);
}

How to concatenate multiple list value and generate unique name

I have a list object with 3-4 item like (color, size, unit) and every list has another list like color list have multiple value (red, green, blue). I want to generate names like (red-xl-pcs)(red-xxl-pcs)(blue-xl-pcs)(blue-xxl-pcs)
My Model:
public class Attributes
{
public int AttributeId { get; set; }
public string AttributeName { get; set; }
public IEnumerable<AttributesValue> AttributesValues { get; set; }
}
public class AttributesValue
{
public int AttributeValueId { get; set; }
public string AttributeValueName { get; set; }
}
My controller:
public async Task<ActionResult> GetProductAttribute(int productId)
{
LoadSession();
var productAttributes = await _objProductDal.GetProductAttribute(productId, _strWareHouseId, _strShopId);
foreach (var attribute in productAttributes)
{
attribute.AttributesValues = await _objProductDal.GetProductAttributeValue(productId, attribute.AttributeId, _strWareHouseId, _strShopId);
}
return PartialView("_AttributeTablePartial", productAttributes);
}
My output is like this:
Now I want another list of names concatenated with all value names like:
(12/y - cotton - green), (12/y - cotton - yellow) .... it will generate 8 unique product names.
How can I do this?
Is this what you're after? Iterating over each list and combining all possibilities?
var first = new List<string> { "one", "two" };
var second = new List<string> { "middle" };
var third = new List<string> { "a", "b", "c", "d" };
var all = new List<List<string>> { first, second, third };
List<string> GetIds(List<List<string>> remaining)
{
if (remaining.Count() == 1) return remaining.First();
else
{
var current = remaining.First();
List<string> outputs = new List<string>();
List<string> ids = GetIds(remaining.Skip(1).ToList());
foreach (var cur in current)
foreach (var id in ids)
outputs.Add(cur + " - " + id);
return outputs;
}
}
var names = GetIds(all);
foreach (var name in names)
{
Console.WriteLine(name);
}
Console.Read();
results in the following:
one - middle - a
one - middle - b
one - middle - c
one - middle - d
two - middle - a
two - middle - b
two - middle - c
two - middle - d
copied and slightly adapted from Generate all Combinations from Multiple (n) Lists
Here is method that uses nested functions to stringify the objects:
public static string GetUniqueName(IEnumerable<Attributes> source)
{
return "[{" + String.Join("},{", source.Select(AttributeToString)) + "}]";
string AttributeToString(Attributes a)
{
return a.AttributeId + ":" + a.AttributeName + "[" + String.Join(",",
a.AttributesValues.Select(ValueToString)) + "]";
}
string ValueToString(AttributesValue av)
{
return av.AttributeValueId + ":" + av.AttributeValueName;
}
}
Usage example:
var productAttributes = new string[] {"Car", "Bike"}.Select((s, i) => new Attributes()
{
AttributeId = i + 1,
AttributeName = s,
AttributesValues = new AttributesValue[]
{
new AttributesValue{AttributeValueId = 1, AttributeValueName = s + "Attr1"},
new AttributesValue{AttributeValueId = 2, AttributeValueName = s + "Attr2"},
}
});
Console.WriteLine(GetUniqueName(productAttributes));
Output:
[{1:Car[1:CarAttr1,2:CarAttr2]},{2:Bike[1:BikeAttr1,2:BikeAttr2]}]

Sorting list of objects by NextObjectId

I have list of objects that have properties Id and NextObjectId. NextObjectId is referring to Id of another object telling to that object it is supposed to be after the object it's referring to. Object Ids are random and not in any particular order.
Can you show the most efficient way how to organize list of objects from random order to ascending according to NextObjectId?
If I understand you in the correct way, you have the following relation:
A.Id = B.NextObjectId && B.Id != A.Id.
So you need to sort by Id with Linq.
IEnumerable<AClass> orderedObjects = objects.OrderBy(e => e.Id);
Or if you want to sort by NextObjectId
IEnumerable<AClass> orderedObjects = objects.OrderBy(e => e.NextObjectId);
I made this algorithm. It should do what you wanted to:
class Program
{
private static Random random = new Random();
static void Main(string[] args)
{
//GENERATE A RANDOM LIST FOR THE EXAMPLE
List<Item> items = new List<Item>();
int? lastID = null;
for(int i = 0; i < 100; i++)
{
int id = random.Next();
items.Add(new Item() { ID = id, NextItemID = lastID });
lastID = id;
}
//GENERATE A RANDOM LIST FOR THE EXAMPLE
items = items.OrderBy(x => random.Next()).ToList(); //SHUFFLE THE LIST FOR THE EXAMPLE
items.OrderByNextItemID(); //SORT THE LIST BY NextItemID
}
}
public static class Extensions
{
public static void OrderByNextItemID(this List<Item> items)
{
List<Item> results = new List<Item>();
Item item = items.Where(x => x.NextItemID == null).First();
results.Add(item);
items.Remove(item);
int length = items.Count;
for(int i = 0; i < length; i++)
{
item = items.Where(x => x.NextItemID == results.Last().ID).FirstOrDefault();
if(item != null)
{
results.Add(item);
items.Remove(item);
}
}
results.Reverse();
items.AddRange(results);
}
}
public class Item
{
public int ID { get; set; }
public int? NextItemID { get; set; }
}
Here it is.
If I understood you right, you have a class like this:
public class MyObject
{
public int Id { get; set; }
public int? NextObjectId { get; set; }
}
and List<MyObject> like this one:
List<MyObject> myObjects = new List<MyObject>
{
new MyObject {Id = 5, NextObjectId = 4},//4
new MyObject {Id = 4, NextObjectId = 1},//5
new MyObject {Id = 2, NextObjectId = 87},//1
new MyObject {Id = 70, NextObjectId = 5},//3
new MyObject {Id = 1, NextObjectId = null},//6
new MyObject {Id = 87, NextObjectId = 70}//2
};
Since the NextObjectId refers to the next object's Id, then there must be a NextObjectId with null (or zero), that is the last element, and the item before it is the before last element which its NextObjectId's value equals to the last element's Id. We can build the method OrderChain on this algorithm:
private static IEnumerable<MyObject> OrderChain(List<MyObject> myObjects)
{
var starter = myObjects.FirstOrDefault(x => x.NextObjectId == null);
yield return starter;
var count = myObjects.Count;
while (count > 1)
{
yield return GetCurrent(myObjects, ref starter);
count--;
}
}
private static MyObject GetCurrent(List<MyObject> lst, ref MyObject current)
{
var id = current.Id;
current = lst.SingleOrDefault(x => x.NextObjectId == id);
return current;
}
Then you can sort it like this:
var myOrderedObjects = OrderChain(myObjects).Reverse().ToList();
this gives me this list:
{ Id = 2, NextObjectId = 87 },//1
{ Id = 87, NextObjectId = 70 },//2
{ Id = 70, NextObjectId = 5 },//3
{ Id = 5, NextObjectId = 4 },//4
{ Id = 4, NextObjectId = 1 },//5
{ Id = 1, NextObjectId = null }//6
I'd simply create a dictionary from its object ID to the object. From there, assuming you have a starting object and some way of finishing (e.g. a NextObjectId of null) you can just build the list up. It's not particularly LINQ-based, but it is simple:
YourType item = ...; // Whatever should come first.
var map = objects.ToDictionary(obj => obj.ObjectId);
var list = new List<YourType>();
list.Add(item);
while (item.NextObjectId != null)
{
// TODO: Handle missing objects? (Use TryGetValue instead of the indexer)
item = map[item.NextObjectId];
list.Add(item);
}

Add items with same date to sub list of item main list

I have list of items in main list and main list has sub list and now I need all items in main list where have same date add how items of sub list. Does anyone know any recommendation? Here is sample: https://dl.dropboxusercontent.com/u/40039421/Sn%C3%ADmka.PNG
public class calendarData
{
public string name { get; set; }
public string shortName { get; set; }
public DateTime HOURstartDT { get; set; }
}
public static List<List<calendarData>> getCalList()
{
List<calendarData> lessons = new List<calendarData>();
for (int i = 0; i < test.getResp().GetStudies(0).LessonsList.Count; i++)
{
for (int j = 0; j < test.getResp().GetStudies(0).GetLessons(i).HoursCount; j++)
{
lessons.Add(new calendarData
{
name = test.getResp().GetStudies(0).GetLessons(i).Name,
shortName = test.getResp().GetStudies(0).GetLessons(i).Shortname,
HOURstartDT = new DateTime(test.getResp().GetStudies(0).GetLessons(i).GetHours(j).StartTime.Year, test.getResp().GetStudies(0).GetLessons(i).GetHours(j).StartTime.Month, test.getResp().GetStudies(0).GetLessons(i).GetHours(j).StartTime.Day, test.getResp().GetStudies(0).GetLessons(i).GetHours(j).StartTime.Hour, test.getResp().GetStudies(0).GetLessons(i).GetHours(j).StartTime.Minute, 0),
});
}
}
List<List<calendarData>> listOfList=new List<List<calendarData>>();
foreach (calendarData subList in lessons)
{
listOfList.Add(new List<calendarData> {subList});
}
return listOfList;
}
Now I get result: List of 10 item and every items have list with one item. How I can compare HOURstartDT and all items of first list which have same date I need add to sublist of main list.
Example: Main list: item 1: name= "Jon", Date=14.6.2013 item 2:name="Steve", Date=15.6.2013, item 3:name="Michael", Date=14.6.2013
I need items with same date(14.6.2013) add to sub list like items and remove these items from main list.
I am giving you solution with the static data. You can use that.
public List<List<calendarData>> getCalList()
{
List<List<calendarData>> StaticData = new List<List<calendarData>>
{
new List<calendarData>
{
new calendarData
{
name = "Jon Jon",
shortName = "Jon",
HOURstartDT = DateTime.Now.Date.AddDays(-1)
}
},
new List<calendarData>
{
new calendarData
{
name = "Steve Steve",
shortName = "Steve",
HOURstartDT = DateTime.Now.Date
}
},
new List<calendarData>
{
new calendarData
{
name = "Michael Michael",
shortName = "Michael",
HOURstartDT = DateTime.Now.Date
}
}
};
//return StaticData.GroupBy(x => x.FirstOrDefault().HOURstartDT).Select(x => x.ToList()).ToList();
return StaticData.SelectMany(x => x).ToList().GroupBy(x => x.HOURstartDT).Select(x => x.ToList()).ToList();
}
You can try something like this, using linq
var mySubList = lessons.Where(l => l.HOURstartDT == AddYourDataHere);
mySubList.ForEach(item =>
{
lessons.remove(item);
});
Not sure I understand the full question, but this will be a start

Build tree type list by recursively checking parent-child relationship C#

I have One class that has a list of itself so it can be represented in a tree structure.
I am pulling a flat list of these classes and want to unflatten it.
public class Group
{
public int ID {get;set;}
public int? ParentID {get;set;}
public List<Group> Children {get;set;}
}
I want to be able to do the following
List<Group> flatList = GetFlatList() //I CAN ALREADY DO THIS
List<Group> tree = BuildTree(flatList);
The ParentID related to the ID property on its parent group if that wasnt obvious.
EDIT
There is some confusion as to why I am returning a list and not a single object.
I am building a UI element that has a list of items, each of why has a child. So the initial list DOES NOT have a root node. It seems all of the solutions so far do not work.
What this means is I essentially need a list of tree type structures using Group class.
I have no idea why you want your BuildTree method return List<Group> - tree needs to have root node, so you should expect it to return single Group element, not a list.
I would create an extension method on IEnumerable<Group>:
public static class GroupEnumerable
{
public static IList<Group> BuildTree(this IEnumerable<Group> source)
{
var groups = source.GroupBy(i => i.ParentID);
var roots = groups.FirstOrDefault(g => g.Key.HasValue == false).ToList();
if (roots.Count > 0)
{
var dict = groups.Where(g => g.Key.HasValue).ToDictionary(g => g.Key.Value, g => g.ToList());
for (int i = 0; i < roots.Count; i++)
AddChildren(roots[i], dict);
}
return roots;
}
private static void AddChildren(Group node, IDictionary<int, List<Group>> source)
{
if (source.ContainsKey(node.ID))
{
node.Children = source[node.ID];
for (int i = 0; i < node.Children.Count; i++)
AddChildren(node.Children[i], source);
}
else
{
node.Children = new List<Group>();
}
}
}
Usage
var flatList = new List<Group>() {
new Group() { ID = 1, ParentID = null }, // root node
new Group() { ID = 2, ParentID = 1 },
new Group() { ID = 3, ParentID = 1 },
new Group() { ID = 4, ParentID = 3 },
new Group() { ID = 5, ParentID = 4 },
new Group() { ID = 6, ParentID = 4 }
};
var tree = flatList.BuildTree();
Here's how you can do this in one line:
static void BuildTree(List<Group> items)
{
items.ForEach(i => i.Children = items.Where(ch => ch.ParentID == i.ID).ToList());
}
You can just call it like this:
BuildTree(flatList);
If at the end you want to get the nodes whose parent is null (i.e. the top-level nodes), you can simply do this:
static List<Group> BuildTree(List<Group> items)
{
items.ForEach(i => i.Children = items.Where(ch => ch.ParentID == i.ID).ToList());
return items.Where(i => i.ParentID == null).ToList();
}
And if you want to make it an extension method, you can just add this in the method signature:
static List<Group> BuildTree(this List<Group> items)
Then you can call it like this:
var roots = flatList.BuildTree();
I tried solutions suggested and figured out that they give us about O(n^2) complexity.
In my case (I have about 50k items to be built into tree) it was completely unacceptable.
I came to the following solution (assuming that each item has only one parent and all parents exist in the list) with complexity O(n*log(n)) [n times getById, getById has O(log(n)) complexity]:
static List<Item> BuildTreeAndReturnRootNodes(List<Item> flatItems)
{
var byIdLookup = flatItems.ToLookup(i => i.Id);
foreach (var item in flatItems)
{
if (item.ParentId != null)
{
var parent = byIdLookup[item.ParentId.Value].First();
parent.Children.Add(item);
}
}
return flatItems.Where(i => i.ParentId == null).ToList();
}
Full code snippet:
class Program
{
static void Main(string[] args)
{
var flatItems = new List<Item>()
{
new Item(1),
new Item(2),
new Item(3, 1),
new Item(4, 2),
new Item(5, 4),
new Item(6, 3),
new Item(7, 5),
new Item(8, 2),
new Item(9, 3),
new Item(10, 9),
};
var treeNodes = BuildTreeAndReturnRootNodes(flatItems);
foreach (var n in treeNodes)
{
Console.WriteLine(n.Id + " number of children: " + n.Children.Count);
}
}
// Here is the method
static List<Item> BuildTreeAndReturnRootNodes(List<Item> flatItems)
{
var byIdLookup = flatItems.ToLookup(i => i.Id);
foreach (var item in flatItems)
{
if (item.ParentId != null)
{
var parent = byIdLookup[item.ParentId.Value].First();
parent.Children.Add(item);
}
}
return flatItems.Where(i => i.ParentId == null).ToList();
}
class Item
{
public readonly int Id;
public readonly int? ParentId;
public Item(int id, int? parent = null)
{
Id = id;
ParentId = parent;
}
public readonly List<Item> Children = new List<Item>();
}
}
public class Item {
public readonly int Id;
public readonly int ? ParentId;
public Item(int id, int ? parent = null) {
Id = id;
ParentId = parent;
}
public readonly List < Item > Children = new List < Item > ();
}
public class BuildTree {
public static List < Item > BuildTreeAndReturnRootNodes(List < Item > flatItems) {
var byIdLookup = flatItems.ToLookup(i => i.Id);
foreach(var item in flatItems) {
if (item.ParentId != null) {
var parent = byIdLookup[item.ParentId.Value].First();
parent.Children.Add(item);
}
}
return flatItems.Where(i => i.ParentId == null).ToList();
}
}
public class TreeToFlatternBack {
public static IEnumerable < Item > GetNodes(Item node) {
if (node == null) {
yield
break;
}
yield
return node;
foreach(var n in node.Children) {
foreach(var innerN in GetNodes(n)) {
yield
return innerN;
}
}
}
}
class Program {
static void Main(string[] args) {
var flatItems = new List < Item > () {
new Item(1),
new Item(2),
new Item(3, 1),
new Item(4, 2),
new Item(5, 4),
new Item(6, 3),
new Item(7, 5),
new Item(8, 2),
new Item(9, 3),
new Item(10, 9),
};
Console.WriteLine();
Console.WriteLine("--------------------Build a Tree--------------------");
Console.WriteLine();
var treeNodes = BuildTree.BuildTreeAndReturnRootNodes(flatItems);
foreach(var n in treeNodes) {
Console.WriteLine(n.Id + " number of children: " + n.Children.Count);
}
Console.WriteLine();
Console.WriteLine("--------------------Tree Back to Flattern--------------------");
Console.WriteLine();
List < Item > BackToflatItems = new List < Item > ();
foreach(var item in treeNodes) {
BackToflatItems.AddRange(TreeToFlatternBack.GetNodes(item));
}
foreach(var n in BackToflatItems.OrderBy(x => x.Id)) {
Console.WriteLine(n.Id + " number of children: " + n.Children.Count);
}
}
}

Categories

Resources