Grouping and sorting XSLT by key and position()

2019-06-01 19:54发布

问题:

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

回答1:

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:

  1. 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.

  2. 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:

  1. 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.

  2. 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.

  3. 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)
                ]"/>