2

I am trying to build a list that parses my entire xml document. I need to list the numeric names then the alpha names. The list should look something like this.

6
6600 Training
6500 Training

A
Accelerated Training

T
Training

This is a snippet of the xml.

<courses>
    <course>         
        <name>Accelerated Training</name>
    </course>
    <course>        
        <name>6600 Training</name>
    </course>   
         <course>        
        <name>Training</name>
    </course>
    <course>        
        <name>6500 Training</name>
    </course>   

</courses>   

This is the code I am currently using. I found this in another question on the site and have customized it somewhat. Currently it doesn't take into account my need for parsing by number and it also returns out of alphabetical order.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> 

  <xsl:output omit-xml-declaration="yes" indent="yes"/> 
  <xsl:variable name="vLower" select= "'abcdefghijklmnopqrstuvwxyz'"/> 
  <xsl:variable name="vUpper" select= "'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/> 

  <xsl:key name="kTitleBy1stLetter" match="courses/course"  use="substring(name,1,1)"/>    

  <xsl:template match="/*">      

    <xsl:for-each select="course [generate-id() = generate-id(key('kTitleBy1stLetter', substring(name,1,1)) [1] ) ]">        
      <xsl:variable name="v1st" select="substring(name,1,1)"/>        
      <h2><xsl:value-of select="$v1st"/></h2>        
      <div class="{translate($v1st, $vUpper, $vLower)}-content">
        <ul>
          <xsl:for-each select="key('kTitleBy1stLetter',$v1st)">               
            <li><xsl:value-of select="name"/></li>
          </xsl:for-each>          
        </ul>      
      </div>      
    </xsl:for-each>        
  </xsl:template>
</xsl:stylesheet>

3 Answers 3

7

Basically you need to group by first letter and sort by <name>. You are on a good way with your Muenchian grouping approach already.

I would suggest an alternative that's a bit easier on the eye:

<xsl:key name="kInitial" match="course" use="substring(name, 1, 1)" />

<xsl:template match="courses">
  <xsl:apply-templates select="course" mode="initial">
    <xsl:sort select="name" />
  </xsl:apply-templates>
</xsl:template>

<xsl:template match="course" mode="initial">
  <xsl:variable name="initial" select="substring(name, 1, 1)" />
  <xsl:variable name="courses" select="key('kInitial', $initial)" />
  <xsl:if test="generate-id() = generate-id($courses[1])">
    <h2><xsl:value-of select="$initial"/></h2>
    <ul>
      <xsl:apply-templates select="$courses">
        <xsl:sort select="name" />
      </xsl:apply-templates>
    </ul>
  </xsl:if>
</xsl:template>

<xsl:template match="course">
  <li>
    <xsl:value-of select="name"/>
  </li>
</xsl:template>

outputs:

<h2>6</h2>
<ul>
  <li>6500 Training</li>
  <li>6600 Training</li>
</ul>
<h2>A</h2>
<ul>
  <li>Accelerated Training</li>
</ul>
<h2>T</h2>
<ul>
  <li>Training</li>
</ul>

EDIT: For the sake of legibility I left out the upper-casing of the first letter. The correct key would be this (you can't use a variable in a key, hence the literal alphabet strings):

<xsl:key name="kInitial" match="course" use="
  translate(
    substring(name, 1, 1), 
    'abcdefghijklmnopqrstuvwxyz', 
    'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
  )
" />

The same goes of course for the $initial variable in the second template, but here you can in fact use variables again.

EDIT #2: Since sorting is case-sensitive as well, you can use the same expression:

<xsl:sort select="translate(substring(name, 1, 1), $vLower, $vUpper)" />
Sign up to request clarification or add additional context in comments.

6 Comments

Thank you very much. As far as parsing is concerned this is working perfectly. This all in a table and I have been using the code below to create a banding effect. How can I implement this? <xsl:if test="position() mod 2 =1)"> <xsl:attribute name="bgcolor">#e7e7e7</xsl:attribute> </xsl:if>
Where should the coloring go?
It is a banded table and the coloring goes in every other row.
But currently it's not a table, but <h2>, <ul> and <li>. So... where should the coloring go?
Sorry what I posted here is a simplified version of my code. I did figure it out though. I needed to move this chenck of code just above my display cell. <xsl:if test="(position() mod 2 = 1)"> <xsl:attribute name="bgcolor">#e7e7e7</xsl:attribute> </xsl:if>
|
2

An XSLT 2.0 solution:

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="text"/>

    <xsl:template match="/*">
      <xsl:for-each-group select="course"
           group-by="upper-case(substring(name,1,1))">
        <xsl:sort select="current-grouping-key()"/>

        <xsl:sequence select=
           "concat('&#xA;', current-grouping-key())"/>

        <xsl:for-each select="current-group()">
          <xsl:sort select="upper-case(name)"/>
          <xsl:sequence select="concat('&#xA;&#x9;', name)"/>
        </xsl:for-each>
      </xsl:for-each-group>
    </xsl:template>
</xsl:stylesheet>

When the above transformation is applied on the originally-provided XML document:

<courses>
    <course>
        <name>Accelerated Training</name>
    </course>
    <course>
        <name>6600 Training</name>
    </course>
    <course>
        <name>Training</name>
    </course>
    <course>
        <name>6500 Training</name>
    </course>
</courses>

the wanted result is produced (in text format for simplicity -- producing the Html is left as an exercise for the reader :)

6 
    6500 Training 
    6600 Training 
A 
    Accelerated Training 
T 
    Training

Do note:

  1. The use of the <xsl:for-each-group> XSLT 2.0 instruction

  2. The use of the current-grouping-key() and current-group() XSLT 2.0 functions.

  3. The use of the upper-case() XPath 2.0 function

1 Comment

Nice and clean. +1 Some of the 2.0 functions should have been in 1.0 from the start. Especially string processing and grouping is a pain in 1.0.
0

Well the numbers part is tricky if you want anything complex, but based on your ideal output all you're missing is a simple sort on your for-each:

<xsl:sort select="key('kTitleBy1stLetter', substring(name,1,1))" />

caveat: I make no claims about this being the best or only or otherwise method, merely that this works full stop, and uses what you already have.

1 Comment

Oh - I think it would be <xsl:sort select="substring(name, 1, 1)" /> or <xsl:sort select="name" />.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.