Simple group slicing in XSLT
First, let me start with a disclaimer: I'm not an XSLT guru. I know folks that can truly do amazing things with it. Every now and then, though, I can help someone, and I thought I'd share a solution I came up with recently. I'll be happy to see alternate solutions posted in the comments.
So, let's say you have an XML document such as this one:
<group>
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
</group>
You'd like to create "slices" of the items in the group, such that you get them reparented in twos (could be three, four or whatever - we'll go with two items per slice). Any remainder try to fill up as much as possible of the last slice, so the transformed document should look like this.
<group>
<slice>
<item>1</item>
<item>2</item>
</slice>
<slice>
<item>3</item>
<item>4</item>
</slice>
<slice>
<item>5</item>
</slice>
</group>
My solution has three templates: one for processing each item (I do some simple content copying "by hand"), one to process each slice (which has a the current context the first item in the slice), and one to process the whole document. The trick is to use the mod function to pick the beginning item of each slice, and to use the following-sibling axis to grab the ones that come right after.
Here's the XSLT for the solution that produces the desired result.
<xsl:stylesheet
version='1.0'
xmlns:xsl='https://www.w3.org/1999/XSL/Transform'>
<xsl:output method='xml' indent='yes'/>
<!-- Template for a single item in a slice. -->
<xsl:template match='item' mode='inslice'>
<item><xsl:value-of select='text()' /></item>
</xsl:template>
<!-- Template for each slice. -->
<xsl:template match='item' mode='firstofslice'>
<slice>
<xsl:apply-templates select='.' mode='inslice' />
<xsl:apply-templates select='following-sibling::item[1]' mode='inslice' />
<!--
"grow" the slice by adding more of these, but change the 'mod' below:
<xsl:apply-templates select='following-sibling::item[2]' mode='inslice' />
-->
</slice>
</xsl:template>
<xsl:template match='/'>
<group>
<!-- Grab every second item to create a slice. -->
<xsl:apply-templates select='group/item[position() mod 2 = 1]' mode='firstofslice' />
</group>
</xsl:template>
</xsl:stylesheet>
Enjoy!
Comments
Anonymous
June 02, 2009
3 templates! I can name that tune in 1 template :-) This method uses an xsl:for-each construct to get the desired results: <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/> <xsl:template match="/"> <group> <!-- Grab every second item to create a slice. --> <xsl:for-each select="group/item[position() mod 2 = 1]"> <slice> <item> <xsl:value-of select="text()"/> </item> <xsl:if test="following-sibling::item[1]"> <item> <xsl:value-of select="following-sibling::item[1]/text()"/> </item> </xsl:if> </slice> </xsl:for-each> </group> </xsl:template> </xsl:stylesheet>Anonymous
June 03, 2009
Nice work! :) I have the xsl:apply-templates vs. xsl:for-each post in the backlog - I promise I'll get to it soon.