views:

779

answers:

5

Lets say I have this xml:

<categories>
    <category text="Arts">
            <category text="Design"/>
            <category text="Visual Arts"/>
    </category>
    <category text="Business">
            <category text="Business News"/>
            <category text="Careers"/>
            <category text="Investing"/>
    </category>
    <category text="Comedy"/>
</categories>

I want to write a LINQ query that will return the category and it's parent category if it has any.

For example, if I was searching for "Business News" I would want it to return an XElement containing the following:

<category text="Business">
   <category text="Business News" />
</category>

If I only search for "Business", I would just want

<category text="Business" />

So far the best I can do is use LINQ to get the element I'm searching for, then check if the parent of the node I found is the root node and adjust accordingly. Is there a better way?

A: 

I haven't tested this, but it should be something like this:

XDocument xmlFile;

return from c in xmlFile.Descendants("category")
       where c.Attribute("text").Value == "Business News"
       select c.Parent ?? c;

The ?? operator returns the parent XElement, and if that's null the 'c'.

Edit: This solution returns what you want, but I'm not sure if it's the best, because it gets pretty complicated:

var cat = from c in doc.Descendants("category")
          where c.Attribute("text").Value == "Business News"
          let node = c.Parent ?? c
          select c.Parent == null
                     ? c // Parent null, just return child
                     : new XElement(
                           "category",
                           c.Parent.Attributes(), // Copy the attributes
                           c                      // Add single child
                           );
Sander Rijken
That doesn't work, it just returns the entire document. The use of ?? is a good idea though.
Evan
OK, next step, I'll test it! :)
Sander Rijken
It actually returns the parent, with all children, not the entire document.
Sander Rijken
+1  A: 

The easy part is to get the path to the element:

IEnumerable<XElement> elementsInPath = 
    doc.Element("categories")
       .Descendants()
       .Where(p => p.Attribute("text").Value == "Design")
       .AncestorsAndSelf()
       .InDocumentOrder()
       .ToList();

The InDocumentOrder() is there to get the collection in the order of root, child, grandchild. The ToList() is there to avoid any unwanted effects in the next step.

Now, the less beautiful part, which maybe could be done in a more elegant way:

var newdoc = new XDocument();
XContainer elem = newdoc;
foreach (var el in elementsInPath))
{
    el.RemoveNodes();
    elem.Add(el);
    elem = elem.Elements().First();
}

That's it. Since each XElement keeps their child, we have to remove the children from each node in the path, and then we rebuild the tree.

Jonatan Lindén
+1  A: 

The problem's a lot easier if you build an iterator:

public static IEnumerable<XElement> FindElements(XElement d, string test)
{
    foreach (XElement e in d.Descendants()
        .Where(p => p.Attribute("text").Value == test))
    {
        yield return e;
        if (e.Parent != null)
        {
            yield return e.Parent;
        }
    }
}

Use it anywhere you'd use a Linq query, e.g.:

List<XElement> elms = FindElement(d, "Visual Arts").ToList();

or

foreach (XElement elm in FindElements(d, "Visual Arts"))
{
   ...
}

Edit:

I see now that what the above code provides isn't what the questioner asked for. But what the questioner asked for is a little strange, it seems to me, since the XElement he wants returned is a completely new object, not something in the existing document.

Still, the honor is to serve. Gaze on my works, ye mighty, and despair:

XElement result = doc.Descendants()
                     .Where(x => x.Attribute("text").Value == test)
                     .Select(
                         x => x.Parent != null && x.Parent.Attribute("text") != null
                                ? new XElement(
                                        x.Parent.Name,
                                        new XAttribute("text", x.Parent.Attribute("text").Value),
                                        new XElement(
                                            x.Name, 
                                            new XAttribute("text", x.Attribute("text").Value)))
                                : new XElement(
                                    x.Name, 
                                    new XAttribute("text", x.Attribute("text").Value)))
                    .FirstOrDefault();
Robert Rossney
This returns all elements of the parent, like Sander's answer.
Dour High Arch
The revised query works and is more concise than the currently highest-rated one. Good job.
Dour High Arch
+1  A: 

Given the input, and the requirements as stated, this will do what you want:

    public static class MyExtensions
    {
        public static string ParentAndSelf(this XElement self, XElement parent)
        {
            self.Elements().Remove();
            if (parent != null && parent.Name.Equals(self.Name))
            {
                parent.Elements().Remove();
                parent.Add(self);
                return parent.ToString();
            }
            else
                return self.ToString();
        }
    }

    class Program
    {
        [STAThread]
        static void Main()
        {
            string xml = 
            @"<categories>
                <category text=""Arts"">            
                    <category text=""Design""/>            
                    <category text=""Visual Arts""/>    
                </category>    
                <category text=""Business"">            
                    <category text=""Business News""/>            
                    <category text=""Careers""/>            
                    <category text=""Investing""/>    
                </category>    
                <category text=""Comedy""/>
            </categories>";

            XElement doc = XElement.Parse(xml);

            PrintMatch(doc, "Business News");
            PrintMatch(doc, "Business");
        }

        static void PrintMatch(XElement doc, string searchTerm)
        {
            var hit = (from category in doc
                   .DescendantsAndSelf("category")
                       where category.Attributes("text")
                       .FirstOrDefault()
                       .Value.Equals(searchTerm)
                       let parent = category.Parent
                       select category.ParentAndSelf(parent)).SingleOrDefault();

            Console.WriteLine(hit);
            Console.WriteLine();
        }
    }
GalacticJello
A: 
var text = "Car";

var el = from category in x.Descendants("category")
   from attribute in category.Attributes("text")
   where attribute.Value.StartsWith(text)
   select attribute.Parent.Parent;


Console.WriteLine(el.FirstOrDefault());

Output:

<category text="Business">...

This one will work even if there is not such element, or no such attribute.

George Polevoy
Always gives me "Enumeration yielded no results".
Dour High Arch