views:

1141

answers:

7

I've got a text file that looks like this:

{ Id = 1, ParentId = 0, Position = 0, Title = "root" }
{ Id = 2, ParentId = 1, Position = 0, Title = "child 1" }
{ Id = 3, ParentId = 1, Position = 1, Title = "child 2" }
{ Id = 4, ParentId = 1, Position = 2, Title = "child 3" }
{ Id = 5, ParentId = 4, Position = 0, Title = "grandchild 1" }

I'm looking for a generic C# algorithm that will create an object hierarchy from this. A "Hierarchize" function, if you will, that turns this data into an object hierarchy.

Any ideas?

edit I've already parsed the file into .NET objects:

class Node
{
    public int Id { get; }
    public int ParentId { get; }
    public int Position { get; }
    public string Title { get; }
}

Now I need to actually arrange the objects into an object graph.

A: 

Are you certain the last line's ParentID is 1? The title says grandchild, but it would be a child of "root" if i'm reading things correctly.

Donblas
My mistake, that was a typo. I've updated the post.
Judah Himango
+7  A: 

Hmm... I don't quite see how that works. How can 2 and 5 both have parent=1, position=0? Should 5 have parent 2, 3 or 4?

Okay, this new version goes through the all the nodes three times:

  • Load all nodes and put them into the a map
  • Associate each node with its parent
  • Sort each node's child by position

It's not well-encapsulated, nicely error checking etc - but it works.

using System;
using System.Collections.Generic;
using System.IO;

public class Node
{
    private static readonly char[] Braces = "{}".ToCharArray();
    private static readonly char[] StringTrim = "\" ".ToCharArray();

    public Node Parent { get; set; }
    public int ParentId { get; private set; }
    public int Id { get; private set; }
    public string Title { get; private set; }
    public int Position { get; private set; }
    private readonly List<Node> children = new List<Node>();
    public List<Node> Children { get { return children; } }

    public static Node FromLine(string line)
    {
        Node node = new Node();
        line = line.Trim(Braces);
        string[] bits = line.Split(',');
        foreach (string bit in bits)
        {
            string[] keyValue = bit.Split('=');
            string key = keyValue[0].Trim();
            string value = keyValue[1].Trim();
            switch (key)
            {
                case "Id":
                    node.Id = int.Parse(value);
                    break;
                case "ParentId":
                    node.ParentId = int.Parse(value);
                    break;
                case "Position":
                    node.Position = int.Parse(value);
                    break;
                case "Title":
                    node.Title = value.Trim(StringTrim);
                    break;
                default:
                    throw new ArgumentException("Bad line: " + line);
            }
        }
        return node;
    }

    public void Dump()
    {
        int depth = 0;
        Node node = this;
        while (node.Parent != null)
        {
            depth++;
            node = node.Parent;
        }
        Console.WriteLine(new string(' ', depth * 2) + Title);
        foreach (Node child in Children)
        {
            child.Dump();
        }
    }
}

class Test
{       
    static void Main(string[] args)
    {
        var dictionary = new Dictionary<int, Node>();

        using (TextReader reader = File.OpenText("test.txt"))
        {
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                Node node = Node.FromLine(line);
                dictionary[node.Id] = node;
            }
        }
        foreach (Node node in dictionary.Values)
        {
            if (node.ParentId != 0)
            {
                node.Parent = dictionary[node.ParentId];
                node.Parent.Children.Add(node);
            }
        }

        foreach (Node node in dictionary.Values)
        {
            node.Children.Sort((n1, n2) =>
                               n1.Position.CompareTo(n2.Position));
        }

        Node root = dictionary[1];
        root.Dump();
    }
}

Sample text file:

{ Id = 5, ParentId = 4, Position = 0, Title = "grandchild 1" }
{ Id = 2, ParentId = 1, Position = 0, Title = "child 1" }
{ Id = 4, ParentId = 1, Position = 2, Title = "child 3" }
{ Id = 3, ParentId = 1, Position = 1, Title = "child 2" }
{ Id = 1, ParentId = 0, Position = 0, Title = "root" }

Output:

root
  child 1
  child 2
  child 3
    grandchild 1
Jon Skeet
Jon, your code doesn't read the text file. You magically transformed the text file (data) into source code. That sort of eliminates or ignores half the problem.
Cheeso
@Cheeso: And this is why I asked you to specify if you need this as well in the comments to the question. Parsing the text file is a completely different issue, and to me this hole thing smells like homework.
pbz
@Cheeso: I'd assumed you could do that side of it. Is the text file really in exactly that format? How are strings escaped? I'll edit the answer to make it parse *exactly* the format in the question, but we really need more information.
Jon Skeet
Okay, I've put a *very* primitive parser in, which assumes there's nothing interesting like string escaping etc. I still ignore the Position, assuming things will come out in the right order, and I still assume that the parent comes before its children. Oh, and I still assume that the sample you've given has a typo in it... but other than that, it does actually work.
Jon Skeet
Jon, my fault, that was a typo. I've updated the post so that ID 5 is a proper grandchild.
Judah Himango
I'm afraid the answer is no, the parents do not always appear before the children. And the children do not always appear in order.
Judah Himango
Okay. Fixing...
Jon Skeet
Also, note that I don't need the text file parsed; I've already got the data loaded into C# objects. I've updated the post with this information. My problem is an algorithm that looks at all these objects and assembles them into a hierarchical structure.
Judah Himango
Okay, in that case you can get rid of the parsing bit from my answer, but the rest stands.
Jon Skeet
Alright, I'll give it a spin. Thanks Jon.
Judah Himango
+1  A: 

Once you have the file parsed in you can follow this blog on how to assemble the objects into a hierarchy using LINQ.

Jeremy
Interesting, but it appears Omer Van Kloeten's implementation doesn't care about ordering within the parents.
Judah Himango
+1  A: 
class Node {
    public int Id { get;set;  }
    public int ParentId { get;set;  }
    public int Position { get;set;  }
    public string Title { get;set;  }
    public IEnumerable<Node> Children { get; set; }

    public override string ToString() { return ToString(0); }
    public string ToString(int depth) {
        return "\n" + new string(' ', depth * 2) + Title + (
            Children.Count() == 0 ? "" :
            string.Join("", Children
                .Select(node => node.ToString(depth + 1))
                .ToArray()
            );
    }
}
class Program {
    static void Main(string[] args) {
        var data = new[] {
            new Node{ Id = 1, ParentId = 0, Position = 0, Title = "root" },
            new Node{ Id = 2, ParentId = 1, Position = 0, Title = "child 1" },
            new Node{ Id = 3, ParentId = 1, Position = 1, Title = "child 2" },
            new Node{ Id = 4, ParentId = 1, Position = 2, Title = "child 3" },
            new Node{ Id = 5, ParentId = 3, Position = 0, Title = "grandchild 1" }
        };
        Func<Node, Node> transform = null;
        transform = node => new Node {
            Title = node.Title,
            Id = node.Id,
            ParentId = node.ParentId,
            Position = node.Position,
            Children = (
                from child in data
                where child.ParentId == node.Id
                orderby child.Position
                select transform(child))
        };
        Console.WriteLine(transform(data[0]));
    }
}

result:

root
  child 1
  child 2
    grandchild 1
  child 3
Jimmy
Jimmy, it's a text file, not source code!
Cheeso
+1  A: 

I assume that your example incorrectly gives the wrong parent ID to object #5. This should cover it. Caveats: Assumes the "topmost" node always has a parent ID of zero. Ignores any nodes that aren't eventually descended from the topmost node. Behavior will be odd if presented with duplicate IDs.

public class FlatObj
{
    public int Id;
    public int ParentId;
    public int Position;
    public string Title;
}

public class Node
{
    public int ID;
    public string Title;
    public IList<Node> Children;

    public Node(FlatObject baseObject, IList<Node> children)
    {
        this.ID = baseObject.Id;
        this.Title = baseObject.Title;
        this.Children = children;
    }
}

public static Node CreateHierarchy(IEnumerable<FlatObject> objects)
{
    var families = objects.ToLookup(x => x.ParentId);
    var topmost = families[0].Single();

    Func<int, IList<Node>> Children = null;

    Children = (parentID) => families[parentID]
        .OrderBy(x => x.Position)
        .Select(x => new Node(x, Children(x.Id))).ToList();

    return new Node(topmost, Children(topmost.Id));
}

public static void Test()
{
    List<FlatObj> objects = new List<FlatObj> {
    new FlatObj { Id = 1, ParentId = 0, Position = 0, Title = "root" },
    new FlatObj { Id = 2, ParentId = 1, Position = 0, Title = "child 1" },
    new FlatObj { Id = 3, ParentId = 1, Position = 1, Title = "child 2" },
    new FlatObj { Id = 4, ParentId = 1, Position = 2, Title = "child 3" },
    new FlatObj { Id = 5, ParentId = 2, Position = 0, Title = "grandchild" }};

    var nodes = CreateHierarchy(objects);
}
mquander
I think you are missing the point, in two ways. You are focused on the ParentId of one of the lines in the text file. Let's for a moment just assume that there was a typo in the original question. Regardless, the question remains - how to hydrate an object graph from that kind of data. The answer you provided uses source code that looks like the text file. ???? It amounts to a half answer. You have completely ignored the problem of parsing the file. It may seem simple to you but it is not trivial.
Cheeso
I thought his question assumed that the file was already parsed into some object similar to my FlatObj, and he was presenting us an abstract representation of the file's contents.
mquander
mquander is correct, my question assumes the text file is already parsed into some object data. I will update the question to clarify this. mquander, thanks for this solution, I'll give it a spin. If it turns out good, I'll mark this as the correct answer.
Judah Himango
+5  A: 

Many thanks to Jon and to mquander - you guys gave me enough information to help me solve this in a proper, generic way. Here's my solution, a single generic extension method that converts objects into hierarchy form:

public static IEnumerable<Node<T>> Hierarchize<T, TKey, TOrderKey>(
    this IEnumerable<T> elements, 
    TKey topMostKey, 
    Func<T, TKey> keySelector, 
    Func<T, TKey> parentKeySelector, 
    Func<T, TOrderKey> orderingKeySelector)
{
    var families = elements.ToLookup(parentKeySelector);
    Func<TKey, IEnumerable<Node<T>>> Children = null;
    Children = parentId => families[parentId]
        .OrderBy(orderingKeySelector)
        .Select(x => new Node<T>(x, Children(keySelector(x))));

    return Children(topMostKey);
}

Utilizes this small data node class:

public class Node<T>
{
    public T DataObject { get; private set; }
    public IList<Node<T>> Children { get; private set; }

    public Node(T dataObject, IEnumerable<Node<T>> children)
    {
        this.DataObject = dataObject;
        this.Children = new List<Node<T>>(children);
    }
}

It's generic enough to work for a variety of problems, including my text file issue. Nifty!

Judah Himango
Great, glad you worked out a solution!
mquander
A: 

Judah and all, thank you all for ending up such a generic way to get hierarchy structure which makes my life much easier. I am wonder if you can help me to implement a little bit more by adding a level property (aka depth). The code by Omer van (http://weblogs.asp.net/okloeten/archive/2006/07/09/Hierarchical-Linq-Queries.aspx) has level property but I do not know how to integrate two methods together.

qiuyl