I am trying to display data sorted alphabetically so that items that begin with the same letter are in separate columns. These columns can hold a maximum of 10 items before a new column is started. I can successfully divide the data up alphabetically AND divide it up by number of items per column but I am struggling to combine the 2:
Divided alphabetically:
<xsl:template match="/">
<xsl:key name="node-by-first-letter" match="node" use="substring(@email, 1, 1)" />
<div class="scroller-panel">
<xsl:for-each select="msxml:node-set($members)/node[count(. | key('node-by-first-letter', substring(@email, 1, 1))[1]) = 1]">
<xsl:sort select="@email" order="ascending"/>
<xsl:apply-templates select="." mode="group" />
</xsl:for-each></div></xsl:template>
<xsl:template match="node" mode="group">
<div class="column-312 scroller-item people-search-column fade-panel">
<h2>
<xsl:value-of select="Exslt.ExsltStrings:uppercase(substring(@email,1,1))"/>
</h2>
<ul class="stripe-list">
<xsl:apply-templates select="key('node-by-first-letter', substring(@email, 1, 1))" mode="item">
<xsl:sort select="@email" />
</xsl:apply-templates>
</ul>
</div>
</xsl:template>
<xsl:template match="node" mode="item">
<li>
<a href="4.0.1.person.profile.html">
<xsl:value-of select="@email"/>
</a>
</li>
</xsl:template>
Divided by max items per column:
<xsl:for-each select="msxml:node-set($members)/members/member[position() mod 10 = 1]">
<ul>
<xsl:for-each select=". | following-sibling::*[not(position() >= 10)]">
<li>
<xsl:value-of select="@email"/>
</li>
</xsl:for-each>
</ul>
</xsl:for-each>
Preferred output is like:
http://rookery9.aviary.com.s3.amazonaws.com/9676500/9676792_3580_625x625.jpg
I. XSLT 2.0 Solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pColLength" as="xs:integer" select="10"/>
<xsl:template match="/*">
<names>
<xsl:for-each-group select="name"
group-by="substring(.,1,1)">
<xsl:sort select="current-grouping-key()"/>
<xsl:for-each-group select="current-group()"
group-by="(position()-1) idiv $pColLength">
<column>
<xsl:copy-of select="current-group()"/>
</column>
</xsl:for-each-group>
</xsl:for-each-group>
</names>
</xsl:template>
</xsl:stylesheet>
when applied on this XML document (as no such was provided in the question!!!):
<names>
<name>T A</name>
<name>T B</name>
<name>T C</name>
<name>T D</name>
<name>T E</name>
<name>T F</name>
<name>T G</name>
<name>T H</name>
<name>T I</name>
<name>T J</name>
<name>T K</name>
<name>T L</name>
<name>A A</name>
<name>A B</name>
<name>A C</name>
<name>A D</name>
<name>A E</name>
<name>A F</name>
<name>A G</name>
<name>A H</name>
<name>A I</name>
<name>A J</name>
<name>A K</name>
<name>A L</name>
<name>X A</name>
<name>X B</name>
<name>X C</name>
<name>X D</name>
<name>X E</name>
<name>X F</name>
<name>X G</name>
<name>X H</name>
<name>X I</name>
<name>X J</name>
<name>X K</name>
<name>X L</name>
<name>R A</name>
<name>R B</name>
<name>R C</name>
<name>R D</name>
<name>R E</name>
<name>R F</name>
<name>R G</name>
<name>R H</name>
<name>R I</name>
<name>R J</name>
<name>R K</name>
<name>R L</name>
</names>
produces the desired output -- names sorted by starting first letter and put into columns of 10 items each:
<names>
<column>
<name>A A</name>
<name>A B</name>
<name>A C</name>
<name>A D</name>
<name>A E</name>
<name>A F</name>
<name>A G</name>
<name>A H</name>
<name>A I</name>
<name>A J</name>
</column>
<column>
<name>A K</name>
<name>A L</name>
</column>
<column>
<name>R A</name>
<name>R B</name>
<name>R C</name>
<name>R D</name>
<name>R E</name>
<name>R F</name>
<name>R G</name>
<name>R H</name>
<name>R I</name>
<name>R J</name>
</column>
<column>
<name>R K</name>
<name>R L</name>
</column>
<column>
<name>T A</name>
<name>T B</name>
<name>T C</name>
<name>T D</name>
<name>T E</name>
<name>T F</name>
<name>T G</name>
<name>T H</name>
<name>T I</name>
<name>T J</name>
</column>
<column>
<name>T K</name>
<name>T L</name>
</column>
<column>
<name>X A</name>
<name>X B</name>
<name>X C</name>
<name>X D</name>
<name>X E</name>
<name>X F</name>
<name>X G</name>
<name>X H</name>
<name>X I</name>
<name>X J</name>
</column>
<column>
<name>X K</name>
<name>X L</name>
</column>
</names>
Explanation:
Nested xsl:for-each-group
-- first grouped by the starting character, then for each such determined and sorted group -- by the number of the column in which its items should be.
Use of the standard XSLT 2.0 functions current-grouping-key()
and current-group()
.
II.XSLT 1.0 Solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pColLength" select="10"/>
<xsl:key name="kStarting" match="name"
use="substring(.,1,1)"/>
<xsl:template match="/*">
<names>
<xsl:for-each select=
"name
[generate-id()
=
generate-id(key('kStarting', substring(.,1,1))[1])
]
">
<xsl:sort select="substring(.,1,1)"/>
<xsl:variable name="vgroupNames" select=
"key('kStarting', substring(.,1,1))"/>
<xsl:apply-templates select="$vgroupNames[1]">
<xsl:with-param name="pGroup" select="$vgroupNames"/>
<xsl:with-param name="pGroupLength" select=
"count($vgroupNames)"/>
</xsl:apply-templates>
</xsl:for-each>
</names>
</xsl:template>
<xsl:template match="name">
<xsl:param name="pGroup"/>
<xsl:param name="pGroupLength"/>
<xsl:param name="pInd" select="1"/>
<xsl:if test="not($pInd > $pGroupLength)">
<column>
<xsl:copy-of select=
"$pGroup
[position() >= $pInd
and
not(position() > $pInd + $pColLength -1)
]"/>
</column>
<xsl:apply-templates select=
"$pGroup[position() = $pInd + $pColLength]">
<xsl:with-param name="pGroup" select="$pGroup"/>
<xsl:with-param name="pGroupLength" select="$pGroupLength"/>
<xsl:with-param name="pInd" select="$pInd + $pColLength"/>
</xsl:apply-templates>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
when applied on the same XML document (as above), the same desired output is produced -- names sorted by starting first letter and put into columns of 10 items each:
<names>
<column>
<name>A A</name>
<name>A B</name>
<name>A C</name>
<name>A D</name>
<name>A E</name>
<name>A F</name>
<name>A G</name>
<name>A H</name>
<name>A I</name>
<name>A J</name>
</column>
<column>
<name>A K</name>
<name>A L</name>
</column>
<column>
<name>R A</name>
<name>R B</name>
<name>R C</name>
<name>R D</name>
<name>R E</name>
<name>R F</name>
<name>R G</name>
<name>R H</name>
<name>R I</name>
<name>R J</name>
</column>
<column>
<name>R K</name>
<name>R L</name>
</column>
<column>
<name>T A</name>
<name>T B</name>
<name>T C</name>
<name>T D</name>
<name>T E</name>
<name>T F</name>
<name>T G</name>
<name>T H</name>
<name>T I</name>
<name>T J</name>
</column>
<column>
<name>T K</name>
<name>T L</name>
</column>
<column>
<name>X A</name>
<name>X B</name>
<name>X C</name>
<name>X D</name>
<name>X E</name>
<name>X F</name>
<name>X G</name>
<name>X H</name>
<name>X I</name>
<name>X J</name>
</column>
<column>
<name>X K</name>
<name>X L</name>
</column>
</names>
Explanation:
Using the Muenchian grouping method, plus sorting, we obtain (in sorted order) each group of name
elements consisting of all names starting with the same character.
Every group of name
elements as obtained above is processed by applying templates to its first name
element. The whole group, its length and the index of the name
element in the group (default = 1) are passed as parameters.
The template matching a name
element is guaranteed to be applied only on a starting element within a column. It creates a new column
element and copies in it all name
elements for this column (starting from index $pInd
and ending at index $pInd+$pColLength -1
. There is no requirement that these elements should be siblings (and they aren't). If not just copying but also additional processing is required for each name
, this can be done here by replacing the <xsl:copy-of>
instruction with:
-
<xsl:apply-templates mode="process" select=
"$pGroup
[position() >= $pInd
and
not(position() > $pInd + $pColLength -1)
]"/>