tags:

views:

206

answers:

4
+3  Q: 

XSLT Grouping

I have a simple XML with two levels (Header and Line) of tags such as:

<?xml version="1.0"?>
<Header>
    <line>Line 1</line>
    <line>Line 2</line>
    <line>Line 3</line>
    <line>Line 4</line>
    <line>Line 5</line>
    <line>Line 6</line>
    <line>Line 7</line>
    <line>Line 8</line>
    <line>Line 9</line>
</Header>

I need to group the lines on sets of X (X=3 for example) lines so that my output is the following:

<?xml version="1.0"?>
<Header>
    <set>
     <line>Line 1</line>
     <line>Line 2</line>
     <line>Line 3</line>
    </set>
    <set>
     <line>Line 4</line>
     <line>Line 5</line>
     <line>Line 6</line>
    </set>
    <set>
     <line>Line 7</line>
     <line>Line 8</line>
     <line>Line 9</line>
    </set>
</Header>

How do I write a XSLT that can do this kind of transformation?

Thanks!

O

+1  A: 

In general in XSLT if you want to create a heirarchy from a list you can make use of the preceding-sibling and following-sibling keywords. This is easist if there is a marker entry between sets.

As you don't have a marker as such in this case I imagine a solution could involve the following-sibling keyword and the mod operator. The mod providing the division between sets.

I haven't tried it but that would be my first start.

xslt is normally a good place to start with understanding the different keywords.

morechilli
+2  A: 

The following transformation produces the required result:

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

 <xsl:variable name="vN" select="3"/>

  <xsl:template match="/*">
    <xsl:copy>
      <xsl:apply-templates 
           select="line[position() mod $vN = 1]"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="line">
    <set>
      <xsl:apply-templates mode="copy" select= 
       ". 
       | 
        following-sibling::line[position() &lt; $vN]"/>
    </set>
  </xsl:template>

    <xsl:template match="line" mode="copy">
      <xsl:copy-of select="."/>
    </xsl:template>
</xsl:stylesheet>

When applied on the provided XML document:

<Header>
    <line>Line 1</line>
    <line>Line 2</line>
    <line>Line 3</line>
    <line>Line 4</line>
    <line>Line 5</line>
    <line>Line 6</line>
    <line>Line 7</line>
    <line>Line 8</line>
    <line>Line 9</line>
</Header>

the result is:

<Header>
  <set>
    <line>Line 1</line>
    <line>Line 2</line>
    <line>Line 3</line>
  </set>
  <set>
    <line>Line 4</line>
    <line>Line 5</line>
    <line>Line 6</line>
  </set>
  <set>
    <line>Line 7</line>
    <line>Line 8</line>
    <line>Line 9</line>
  </set>
</Header>

Do note the following:

  1. The use of the XPath mod operator to find out the first line element in every group of vN elements.

  2. The use of modes, in order to be able to process different line elements by different templates

Dimitre Novatchev
it worked after I changed <xsl:apply-templates select="line[position() mod $vN = 1]"/>to<xsl:apply-templates select="//line[position() mod $vN = 1]"/>
Otavio
You're right Dimitre! I've just found my mistake and implemented the same concept in my real problem and it seems to work fine.Thanks again! (I'll remove the previous comment)
Otavio
A: 

That should be possible. Has the desired output:

<?xml version='1.0' encoding='UTF-8'?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                version="1.0">

  <xsl:template match="/Header">
    <Header>
      <xsl:for-each select="line">
        <xsl:if test="not(number()=0) and position() mod 3 = 0">
          <set>
            <xsl:variable name="pos" select="position()"/>
            <line><xsl:value-of select="../line[position()=($pos -2)]"/></line>
            <line><xsl:value-of select="../line[position()=($pos -1)]"/></line>
            <line><xsl:value-of select="text()"/></line>
            </set>
        </xsl:if>

      </xsl:for-each>
    </Header>
  </xsl:template>

</xsl:stylesheet>

(The $pos-1, $pos-2 thing is not very pretty)

Johannes Weiß
A: 

http://www.xml.com/pub/a/2003/11/05/tr.html shows a slightly less ugly way of doing this using XSLT 2.0. The key element is this one:

<xsl:for-each-group select="*" group-ending-with="*[position() mod 3 = 0]">

Bill Michell