FRassetto
FRassetto

Reputation: 348

Compare Node Names in every level of XML

I am developing a app with C# and trying to complete a XML that I have got from a JSON. And for the XML to be valid for my app, I need to group the elements with the same name under a father element. For example, I got this XML

<root>
<row>
    <id>0001</id>
    <type>credit</type>
    <investment>1000</investment>
    <ppr>0.83</ppr>
    <candidate>
        <id>5001</id>
        <name>Hugo</name>
    </candidate>
    <candidate>
        <id>5002</id>
        <name>Jack</name>
    </candidate>
    <candidate>
        <id>5005</id>
        <name>Kate</name>
    </candidate>
</row>

And I need to group all the elements with the name candidate, under a father node candidates, like this

<root>
<row>
    <id>0001</id>
    <type>credit</type>
    <investment>1000</investment>
    <ppr>0.83</ppr>
<candidates>
        <candidate>
                <id>5001</id>
                <name>Hugo</name>
        </candidate>
        <candidate>
                <id>5002</id>
                <name>Jack</name>
        </candidate>
        <candidate>
                <id>5005</id>
                <name>Kate</name>
        </candidate>
</candidates>
</row>

But here is my problem: I don't know the names that I can receive from the JSON. So I need to do this comparison and complete the XML without knowing the "candidate" node name. I need this for any name that I can receive.

Also in this example the XML only has 2 levels, but it can have any number of levels. I can iterate over the XML without problem with this function:

public void findAllNodes(XmlNode node)
{ 
  Console.WriteLine(node.Name);
  foreach (XmlNode n in node.ChildNodes)
    findAllNodes(n); 
}

How can I make the comparison and group the nodes?

Upvotes: 0

Views: 102

Answers (2)

Michael Kay
Michael Kay

Reputation: 163342

Here's an XSLT 2.0 solution:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:output indent="yes"/>

    <xsl:template match="*[*]">
        <xsl:copy>
            <xsl:for-each-group select="*" group-adjacent="node-name(.)">
                <xsl:choose>
                    <xsl:when test="count(current-group()) > 1">
                        <xsl:element name="{name()}s" namespace="{namespace-uri()}">
                            <xsl:apply-templates select="current-group()"/>
                        </xsl:element>
                    </xsl:when>
                    <xsl:otherwise>
                        <xsl:apply-templates select="current-group()"/>
                    </xsl:otherwise>
                </xsl:choose>
            </xsl:for-each-group>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="*">
        <xsl:copy-of select="."/>
    </xsl:template>
</xsl:stylesheet>

Output:

<?xml version="1.0" encoding="UTF-8"?>
<root>
   <row>
      <id>0001</id>
      <type>credit</type>
      <investment>1000</investment>
      <ppr>0.83</ppr>
      <candidates>
         <candidate>
            <id>5001</id>
            <name>Hugo</name>
         </candidate>
         <candidate>
            <id>5002</id>
            <name>Jack</name>
         </candidate>
         <candidate>
            <id>5005</id>
            <name>Kate</name>
         </candidate>
      </candidates>
   </row>
</root>

Limitations

  • It doesn't handle mixed content (elements with children plus text content)

  • It drops attributes (easily fixed)

Upvotes: 0

Charles Mager
Charles Mager

Reputation: 26213

A fairly naive implementation could use LINQ to group elements by name and add a parent element for those that have more than 1 item in a group. This would be recursive, so child elements of an element were grouped until the tree was exhausted.

The naive-ness is that such a solution would break if there were mixed content elements, and it would group elements that weren't siblings (basically, both issues will result in things ending up in the wrong order). It should give you a good start, and could be enough for your purposes.

private static IEnumerable<XElement> GroupElements(IEnumerable<XElement> elements)
{
    var elementsByName = elements.GroupBy(x => x.Name);

    foreach (var grouping in elementsByName)
    {
        var transformed = grouping.Select(e =>
            new XElement(e.Name,
                GroupElements(e.Elements()),
                e.Attributes(),
                e.Nodes().OfType<XText>()));

        if (grouping.Count() == 1)
        {
            yield return transformed.Single();
        }
        else
        {
            var groupName = grouping.Key + "s";
            yield return new XElement(groupName, transformed);
        }
    }
}

You can use this by parsing/loading your existing XML and then transforming the root elements and creating a new document from those:

var original = XDocument.Parse(xml);
var grouped = new XDocument(GroupElements(original.Elements()));

See this fiddle for a working demo.

Upvotes: 2

Related Questions