Search code examples
xmlxsltxslt-grouping

XSLT split result in groups of 3 while ignoring input tree structure


I studied the solution for "XSLT split result in groups of 3" with interest. The elements in the cited example are all under one node. When I extended the example data to contain 2 branches of , like it is shown below,

<Root>
    <nums>
        <num>01</num>
        <num>02</num>
        <num>03</num>
        <num>04</num>
    </nums>
    <nums>
        <num>11</num>
        <num>12</num>
        <num>13</num>
        <num>14</num>
    </nums>
</Root> 

Using the adapted xslt as follows:

<xsl:stylesheet version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:param name="pGroupSize" select="3"/>

    <xsl:template match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="/Root/*">
        <nums>
            <xsl:apply-templates select=
                "num[position() mod $pGroupSize = 1]"/>
        </nums>
    </xsl:template>

    <xsl:template match="num">
        <group>
            <xsl:copy-of select=
                ".|following-sibling::*
                [not(position() > $pGroupSize -1)]"/>
        </group>
    </xsl:template>
</xsl:stylesheet>

The output is as follows:

<Root>
   <nums>
      <group>
         <num>01</num>
         <num>02</num>
         <num>03</num>
      </group>
      <group>
         <num>04</num>
      </group>
   </nums>
   <nums>
      <group>
         <num>11</num>
         <num>12</num>
         <num>13</num>
      </group>
      <group>
         <num>14</num>
      </group>
   </nums>
</Root>

However, let's say the desired output should take the following form as shown below. How can this be done?

<Root>
   <nums>
      <group>
         <num>01</num>
         <num>02</num>
         <num>03</num>
      </group>
      <group>
         <num>04</num>
         <num>11</num>
         <num>12</num>
      </group>
      <group>
         <num>13</num>
         <num>14</num>
      </group>
   </nums>
</Root>

Thanks!


Solution

  • For XSLT 1 you would need to change your selection to descendant::num and the following-sibling::* to following::num:

    <xsl:template match="/Root">
        <nums>
            <xsl:apply-templates select=
                "descendant::num[position() mod $pGroupSize = 1]"/>
        </nums>
    </xsl:template>
    
    <xsl:template match="num">
        <group>
            <xsl:copy-of select=
                ".|following::num
                [not(position() > $pGroupSize -1)]"/>
        </group>
    </xsl:template>
    

    Full example https://xsltfiddle.liberty-development.net/jyH9rMA

    <xsl:stylesheet version="1.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
        <xsl:output omit-xml-declaration="yes" indent="yes"/>
        <xsl:strip-space elements="*"/>
    
        <xsl:param name="pGroupSize" select="3"/>
    
        <xsl:template match="node()|@*">
            <xsl:copy>
                <xsl:apply-templates select="node()|@*"/>
            </xsl:copy>
        </xsl:template>
    
        <xsl:template match="/Root">
            <nums>
                <xsl:apply-templates select=
                    "descendant::num[position() mod $pGroupSize = 1]"/>
            </nums>
        </xsl:template>
    
        <xsl:template match="num">
            <group>
                <xsl:copy-of select=
                    ".|following::num
                    [not(position() > $pGroupSize -1)]"/>
            </group>
        </xsl:template>
    </xsl:stylesheet>
    

    With the XSLT 2 or 3 for-each-group you can simply select and group as needed:

    <xsl:template match="/Root">
        <xsl:copy>
            <nums>
                <xsl:for-each-group select="nums/num" group-adjacent="(position() - 1) idiv $pGroupSize">
                    <group>
                        <xsl:sequence select="current-group()"/>
                    </group>
                </xsl:for-each-group>
            </nums>            
        </xsl:copy>
    </xsl:template>
    

    https://xsltfiddle.liberty-development.net/jyH9rMA/2:

    <xsl:stylesheet version="3.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
        <xsl:output omit-xml-declaration="yes" indent="yes"/>
        <xsl:strip-space elements="*"/>
    
        <xsl:param name="pGroupSize" select="3"/>
    
        <xsl:mode on-no-match="shallow-copy"/>
    
        <xsl:template match="/Root">
            <xsl:copy>
                <nums>
                    <xsl:for-each-group select="nums/num" group-adjacent="(position() - 1) idiv $pGroupSize">
                        <group>
                            <xsl:sequence select="current-group()"/>
                        </group>
                    </xsl:for-each-group>
                </nums>            
            </xsl:copy>
        </xsl:template>
    
    </xsl:stylesheet>
    

    I have used group-adjacent instead of the group-by (shown in https://stackoverflow.com/a/7320527/252228) as that way the solution adapts better to streaming with Saxon 9.8 EE or Exselt

    <xsl:stylesheet version="3.0"
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    
        <xsl:output omit-xml-declaration="yes" indent="yes"/>
        <xsl:strip-space elements="*"/>
    
        <xsl:param name="pGroupSize" select="3"/>
    
        <xsl:mode on-no-match="shallow-copy" streamable="yes"/>
    
        <xsl:template match="/Root">
            <xsl:copy>
                <nums>
                    <xsl:for-each-group select="nums/num" group-adjacent="(position() - 1) idiv $pGroupSize">
                        <group>
                            <xsl:sequence select="current-group()"/>
                        </group>
                    </xsl:for-each-group>
                </nums>        
            </xsl:copy>
    
        </xsl:template>
    
    </xsl:stylesheet>
    

    so you could use the stylesheet for huge XML inputs without running into memory problems.