简体   繁体   中英

Use XSLT to transform XML based on node count

I am looking to transform a node, based on the number of elements. ie 5, then close the node and re-open a new one. see example below

<root>
  <branch>
    <foo>bar</foo>
    <foo>bar2</foo>
    <foo>bar3</foo>
    <foo>bar4</foo>
    <foo>bar5</foo>
    <foo>bar6</foo>
    <foo>bar7</foo>
  </branch>
<root>

Should become

<root>
  <branch>
    <foo>bar</foo>
    <foo>bar2</foo>
    <foo>bar3</foo>
    <foo>bar4</foo>
    <foo>bar5</foo>
   <branch>
   </branch>
    <foo>bar6</foo>
    <foo>bar7</foo>
  </branch>
<root>

Please help on how this can be done in XSLT.

I. This XSLT 1.0 transformation :

<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:template match="/*">
  <root>
   <xsl:apply-templates/>
  </root>
 </xsl:template>

 <xsl:template match="foo[position() mod 5 = 1]">
  <branch>
    <xsl:copy-of select=
     ". | following-sibling::*[not(position() > 4)]"/>
  </branch>
 </xsl:template>
 <xsl:template match="text()"/>
</xsl:stylesheet>

when applied on the provided XML document (corrected to well-formed):

<root>
    <branch>
        <foo>bar</foo>
        <foo>bar2</foo>
        <foo>bar3</foo>
        <foo>bar4</foo>
        <foo>bar5</foo>
        <foo>bar6</foo>
        <foo>bar7</foo>
    </branch>
</root>

produces the wanted, correct result:

<root>
   <branch>
      <foo>bar</foo>
      <foo>bar2</foo>
      <foo>bar3</foo>
      <foo>bar4</foo>
      <foo>bar5</foo>
   </branch>
   <branch>
      <foo>bar6</foo>
      <foo>bar7</foo>
   </branch>
</root>

Explanation :

This is a case of "positional grouping", where every starting element of a group is the first of a 5-tuple (so its position satisfies: position() mod 5 = 1 .


II. XSLT 2.0 Solution :

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

 <xsl:template match="/*">
  <root>
    <xsl:for-each-group select="*/*"
         group-adjacent="(position() -1) idiv 5">
          <branch>
            <xsl:sequence select="current-group()"/>
          </branch>
    </xsl:for-each-group>
  </root>
 </xsl:template>
</xsl:stylesheet>

when this XSLT 2.0 transformation is applied on the same XML document (above), the same wanted, correct result is produced .

Explanation :

Proper use of the <xsl:for-each-group> XSLT 2.0 instruction with the group-adjacent attribute and the current-group() function.

There is no such operation in XSLT as "opening" or "closing" a node. You've got a wrong idea of the processing model, and until you get it straight in your head you will have great difficulty solving this kind of problem. In particular, you haven't understood that XSLT is (conceptually) creating a result tree, not a file containing lexical XML with start and end tags. What you need to do is to create one node in the result tree corresponding to each group of five nodes in the source tree (or equivalently, to create one node in the result tree corresponding to each node in the source tree whose position is a multiple of 5).

Dimitre's solution is perfectly correct, but I personally think that giving you a working solution without explaining where you went wrong in your thinking is not the best way to answer your question.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM