Search code examples
xsltdistincttransformationappendchildxslt-grouping

XSLT Transforming: Select distinct and appendchild group


<Sections>
    <Products>
      <Transport>
        <TransportSequence>1</TransportSequence>
        <Traveller>001</Traveller>
      </Transport>
      <Transport>
        <TransportSequence>2</TransportSequence>
        <Traveller>001</Traveller>
      </Transport>
    </Products>
  </Sections>
  <Sections>
    <Products>
      <Transport>
        <TransportSequence>1</TransportSequence>
        <Traveller>002</Traveller>
      </Transport>
      <Transport>
        <TransportSequence>2</TransportSequence>
        <Traveller>002</Traveller>
      </Transport>
    </Products>
  </Sections>

I have a specific problem with the ordering of some XML. From the above example I need to change the format so that I select distinct only on the TransportSequence. I then need to assign any 'Traveller' nodes as children to produce something like this:

<Sections>
   <Products>
      <Transport>
         <TransportSequence>1</TransportSequence>
         <Travellers>
            <Traveller>001</Traveller>
            <Traveller>002</Traveller>
         </Travellers>
      </Transport>
      <Transport>
         <TransportSequence>2</TransportSequence>
         <Travellers>
            <Traveller>001</Traveller>
            <Traveller>002</Traveller>
         </Travellers>
      </Transport>
   </Products>
</Sections>

The other problem is that in the Transport node also contains lots of children and grandchildren nodes not shown in this example. There can also be many travllers belonging to a TravellerSequence. There are also many TransportSequence numbers.


Solution

  • Here is an XSLT 2.0 stylesheet to be run with XSLT 2.0 processors like Saxon 9 or AltovaXML:

    <xsl:stylesheet
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      version="2.0">
    
    <xsl:output indent="yes"/>
    <xsl:strip-space elements="*"/>
    
    <xsl:template match="*[Sections]">
      <xsl:copy>
        <Sections>
          <Products>
            <xsl:for-each-group select="Sections/Products/Transport" group-by="TransportSequence">
              <Transport>
                <TransportSequence><xsl:value-of select="current-grouping-key()"/></TransportSequence>
                <Travellers>
                  <xsl:copy-of select="current-group()/Traveller"/>
                </Travellers>
              </Transport>
            </xsl:for-each-group>
          </Products>
        </Sections>
      </xsl:copy>
    </xsl:template>
    
    </xsl:stylesheet>
    

    It transforms

    <Root>
    <Sections>
        <Products>
          <Transport>
            <TransportSequence>1</TransportSequence>
            <Traveller>001</Traveller>
          </Transport>
          <Transport>
            <TransportSequence>2</TransportSequence>
            <Traveller>001</Traveller>
          </Transport>
        </Products>
      </Sections>
      <Sections>
        <Products>
          <Transport>
            <TransportSequence>1</TransportSequence>
            <Traveller>002</Traveller>
          </Transport>
          <Transport>
            <TransportSequence>2</TransportSequence>
            <Traveller>002</Traveller>
          </Transport>
        </Products>
      </Sections>
    </Root>
    

    into

    <Root>
       <Sections>
          <Products>
             <Transport>
                <TransportSequence>1</TransportSequence>
                <Travellers>
                   <Traveller>001</Traveller>
                   <Traveller>002</Traveller>
                </Travellers>
             </Transport>
             <Transport>
                <TransportSequence>2</TransportSequence>
                <Travellers>
                   <Traveller>001</Traveller>
                   <Traveller>002</Traveller>
                </Travellers>
             </Transport>
          </Products>
       </Sections>
    </Root>
    

    [edit] To complete the answer, if you want to use an XSLT 1.0 processor, a solution using Muenchian grouping looks as follows:

    <xsl:stylesheet
      xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
      version="1.0">
    
    <xsl:output indent="yes"/>
    <xsl:strip-space elements="*"/>
    
    <xsl:key name="by-seq" match="Sections/Products/Transport" use="TransportSequence"/>
    
    <xsl:template match="*[Sections]">
      <xsl:copy>
        <Sections>
          <Products>
            <xsl:for-each select="Sections/Products/Transport[generate-id() = generate-id(key('by-seq', TransportSequence)[1])]">
              <Transport>
                <xsl:copy-of select="TransportSequence"/>
                <Travellers>
                  <xsl:copy-of select="key('by-seq', TransportSequence)/Traveller"/>
                </Travellers>
              </Transport>
            </xsl:for-each>
          </Products>
        </Sections>
      </xsl:copy>
    </xsl:template>
    
    </xsl:stylesheet>