0
votes

I have the following input xml and was wondering if XSLT can handle such transformation. If it can, how can it be achieve?

input xml:

<foo>
  <bar>
    <A>xxx</A>
    <B>yyy</B>
    <C>zzz</C>
    <A>aaa</A>
    <B>bbb</B>
    <C>ccc</C>
     ...
     ..
  </bar>
</foo>

output xml:

<data>
   <A>xxx</A>
   <B>yyy</B>
   <C>zzz</C>
</data>
<data>
   <A>aaa</A>
   <B>bbb</B>
   <C>ccc</C>
</data>
....

There could be more repeating A, B, C nodes in the example above. Since the repetition isn't in a repeating parent node, it's not possible to use for-each. I was exploring the option of for-each-group but not sure if that is applicable. Would appreciate any advise.

2
This is a grouping problem (do a search). Solutions are very much different if you're using XSLT 1.0 or 2.0 - please pick one (the tags are meant to be mutually exclusive).michael.hor257k

2 Answers

0
votes

Using XSLT 1.0, following solution is working - tested with your example XML with 6 elements:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/*">
 <xsl:apply-templates select="/foo/bar"/>
</xsl:template>

<xsl:template match="bar">
 <xsl:for-each select="*/node()">
   <xsl:if test="(position()-1) mod(3) = 0">
    <data>
     <xsl:call-template  name="grouping">
      <xsl:with-param name="position" select="position()"/>
      <xsl:with-param name="amount" select="3"/>
     </xsl:call-template>
    </data>
   </xsl:if>
  </xsl:for-each>
</xsl:template>

<xsl:template  name="grouping">
<xsl:param name="position" select="0"/>
<xsl:param name="amount" select="0"/>
<xsl:copy-of select="//bar/*[$position]"/>
   <xsl:if test="$amount > $position or   ($position mod(3) > 0)">
    <xsl:call-template  name="grouping">
      <xsl:with-param name="position" select="$position + 1"/>
      <xsl:with-param name="amount" select="$amount"/>
    </xsl:call-template>
   </xsl:if>
 </xsl:template>
</xsl:stylesheet>

Result:

<?xml version="1.0" encoding="UTF-8"?>
<data>
   <A>xxx</A>
   <B>yyy</B>
   <C>zzz</C>
</data>
<data>
   <A>aaa</A>
   <B>bbb</B>
   <C>ccc</C>
</data>

As short explanation - in the template matching "bar" all element-nodes are processed in

<xsl:for-each select="*/node()">

By using <xsl:if test="(position()-1) mod(3) = 0"> (which is 0 for the 1st node and for every 3rd node) the template named "grouping" is called to provide generating groups of 3 nodes. This template is called with the parameters position - the position of the current node - and amount - the amount of nodes to be copied in each group.
The grouping-template copies the current node - <xsl:copy-of select="//bar/*[$position]"/> - checks if already 3 nodes have been generated - <xsl:if test="$amount > $position or ($position mod(3) > 0)"> - and calls itself again, incrementing as counter the position, so the next node will be copied.
Just in case it's not obvious - the grouping-template will be called for $amount > $position so the first 3 nodes will be copied, and for $position mod(3) > 0 for all following nodes as long as it's not a multiple of 3. In case there would be e.g. a 7th element in your xml, this would be copied as single element in a data-group.

0
votes

Like you were originally thinking, xsl:for-each-group would be perfect; use the group-starting-with attribute.

XML Input

<foo>
    <bar>
        <A>xxx</A>
        <B>yyy</B>
        <C>zzz</C>
        <A>aaa</A>
        <B>bbb</B>
        <C>ccc</C>
        <A>111</A>
        <B>222</B>
        <C>333</C>
    </bar>
</foo>

XSLT 2.0

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

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

    <xsl:template match="/*">
        <xsl:for-each-group select="bar/*" group-starting-with="A">
            <data>
                <xsl:apply-templates select="current-group()"/>
            </data>
        </xsl:for-each-group>       
    </xsl:template>

</xsl:stylesheet>

XML Output (not well-formed as per specified in the original question)

<data>
   <A>xxx</A>
   <B>yyy</B>
   <C>zzz</C>
</data>
<data>
   <A>aaa</A>
   <B>bbb</B>
   <C>ccc</C>
</data>
<data>
   <A>111</A>
   <B>222</B>
   <C>333</C>
</data>