5

Ok, I've got the following XML tree

<root>
    <A>
        <A1>
            <A1A>1000</A1A>
            <A1B>2000</A1B>
            <A1C>3000</A1C>
        </A1>
        <A2>
            <A2A>4000</A2A>
            <A2B>5000</A2B>
        </A2>
    </A>
    <B>
        <B1>
            <B1A>6000</B1A>
        </B1>
    </B>
</root>

From a method receiving an XDocument I want to produce a dictionary where the key is the path (really an XPath) and the value comes from the value in the corresponding leaf.

root/A/A1/A1A    1000
root/A/A1/A1B    2000
root/A/A1/A1C    3000
root/A/A2/A2A    4000
root/A/A2/A2B    5000
root/B/B1/B1A    6000

Seems simple to do in Linq to XML but I can't wrap my head around it.

Stécy
  • 11,951
  • 16
  • 64
  • 89

2 Answers2

9

You can find the leaves by looking for elements that have no descendants:

var doc = XDocument.Load(fileName);
var leaves = 
    from e in doc.Descendants()
    where !e.Elements().Any()
    select e;

I don't know if there is a built-in way to get the path of an element, but you can easily create an extension method to build it:

static class Extensions
{
    public static string Path(this XElement element)
    {
        XElement tmp = element;
        string path = string.Empty;
        while (tmp != null)
        {
            path = "/" + tmp.Name + path;
            tmp = tmp.Parent;
        }
        return path;
    }
}

You can then build the dictionary like this:

var dict = leaves.ToDictionary(e => e.Path(), e => e.Value);
Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • Careful with the indexes of elements, check this question: http://stackoverflow.com/questions/451950/get-the-xpath-to-an-xelement – Alpha Jan 09 '12 at 21:48
  • I was so deep in finding a way to do this in LinqToXml that I completely overlooked how simple it is to just build the path using the parents... doh! I will leave the question open for a little while in case someone knows of a way to do it in LinqToXml. Thanks a lot. – Stécy Jan 09 '12 at 21:49
2

After parsing the XML to an XDocument, which I assume you've already been able to do, use the methods below. Note that the GetPath() implementation is fairly naiive. See this answer for a better implementation.

public Dictionary<string, string> GetLeaves(XDocument doc)
{
    var dict = doc
        .Descendants()
        .Where(e => !e.HasElements)
        .ToDictionary(e => GetPath(e), e.Value);

    return dict;
}

private string GetPath(XElement element)
{
    var nodes = new List<string>();
    var node = element;
    while (node != null)
    {
        nodes.Add(node.Name.ToString());
        node = node.Parent;
    }

    return string.Join("/", Enumerable.Reverse(nodes));
}
Community
  • 1
  • 1
Phil Klein
  • 7,344
  • 3
  • 30
  • 33
  • I don't see the need to use an extension like the other answer did. To me, this answer looks more elegant, readable and maintainable. – daniloquio Aug 10 '15 at 15:07
  • I like the use of `HasElements`. For GetPath, did you consider recursion - `return (element.Parent == null ? "" : GetPath(element.Parent)) + "/" + element.Name.ToString());` –  Nov 06 '18 at 17:19
  • Or use a `Stack` instead of a `List`... then use `Push` instead of `Add` and don't have to use the `Enumerable.Reverse` –  Nov 06 '18 at 17:31