Search code examples
xsltxpath

How do I pair appropriate xml elements with xmlstarlet?


I have two sets of XML nodes, and I want to find elements that have identical "phone" child. For example:

<set1>
  <node>
    <phone>111</phone>
    <name>John</name>
  </node>
  <node>
    <phone>444</phone>
    <name>Amy</name>
  </node>
  <node>
    <phone>777</phone>
    <name>Robin</name>
  </node>
</set1>

<set2>
  <node>
    <phone>111</phone>
    <city>Moscow</city>
  </node>
  <node>
    <phone>444</phone>
    <city>Prag</city>
  </node>
  <node>
    <phone>999</phone>
    <city>Rome</city>
  </node>
</set2>

Now I want to get the following:

<result>
  <node>
    <phone>111</phone>
    <name>John</name>
    <city>Moscow</city>
  </node>
  <node>
    <phone>444</phone>
    <name>Amy</name>
    <city>Prag</city>
  </node>
  <node>
    <phone>777</phone>
    <name>Robin</name>
  </node>
  <node>
    <phone>999</phone>
    <city>Rome</city>
  </node>
</result>

I'm a beginner in xslt, and i managed to merge two xml's and put them in a html table. But this pairing is one level over me.


Solution

  • Use a key

    <xsl:key name="phone" match="node" use="phone"/>
    

    then group with Muenchian grouping as follows:

    <xsl:template match="/">
      <result>
        <xsl:apply-templates select="//node[generate-id() = generate-id(key('phone', phone)[1])]"/>
      </result>
    </xsl:template>
    
    <xsl:template match="node">
      <xsl:copy>
        <xsl:copy-of select="phone"/>
        <xsl:copy-of select="key('phone', phone)/*[not(self::phone)]"/>
      </xsl:copy>
    </xsl:template>
    

    For readability add

    <xsl:output indent="yes"/>
    

    Full example

    input.xml:

    <?xml version="1.0"?>
    <myxml>
      <set1>
        <node>
          <phone>111</phone>
          <name>John</name>
        </node>
        <node>
          <phone>444</phone>
          <name>Amy</name>
        </node>
        <node>
          <phone>777</phone>
          <name>Robin</name>
        </node>
      </set1>
      <set2>
        <node>
          <phone>111</phone>
          <city>Moscow</city>
        </node>
        <node>
          <phone>444</phone>
          <city>Prag</city>
        </node>
        <node>
          <phone>999</phone>
          <city>Rome</city>
        </node>
      </set2>
    </myxml>
    

    stylesheet.xsl:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
        
      <xsl:key name="phone" match="node" use="phone"/>
      
      
      <xsl:template match="/">
        <result>
          <xsl:apply-templates select="//node[generate-id() = generate-id(key('phone', phone)[1])]"/>
        </result>
      </xsl:template>
      
      <xsl:template match="node">
        <xsl:copy>
          <xsl:copy-of select="phone"/>
          <xsl:copy-of select="key('phone', phone)/*[not(self::phone)]"/>
        </xsl:copy>
      </xsl:template>
      
      
      <xsl:output indent="yes"/>
      
    </xsl:stylesheet>
    

    Command:

    xmlstarlet transform stylesheet.xsl input.xml > output.xml
    

    output.xml:

    <?xml version="1.0"?>
    <result>
      <node>
        <phone>111</phone>
        <name>John</name>
        <city>Moscow</city>
      </node>
      <node>
        <phone>444</phone>
        <name>Amy</name>
        <city>Prag</city>
      </node>
      <node>
        <phone>777</phone>
        <name>Robin</name>
      </node>
      <node>
        <phone>999</phone>
        <city>Rome</city>
      </node>
    </result>