xslt sorting grouping

Sorting and Grouping

1. Sort and remove duplicates
2. Sort and xt:node-set
3. Group sort and number
4. Grouping on an element
5. Sorting and grouping refinement
6. Sorting and grouping
7. Add new element every 7th time
8. Sorting and Grouping example
9. How to number on a grouped and sorted set?
10. Multiple output and grouping
11. sort and grouping on 2 different attributes
12. Group and sort by group element occurrences
13. Sorting and grouping

1.

Sort and remove duplicates

Michael Kay

The most powerful solutions unfortunately require extensions.

There's xx:evaluate():

<xsl:for-each select=".....">
  <xsl:sort select="xx:evaluate($sort-key)"/>

and there's stylesheet-defined functions (here in XSLT 2.0 syntax):

<xsl:for-each select=".....">
  <xsl:sort select="xx:my-function($sort-key)"/>
...

<xsl:function name="xx:my-function">
  <xsl:param name="sort-key"/>
...

Within XSLT 1.0 though there are a number of tricks that can be useful. There's the exclusive union trick:

<xsl:sort select="key1[condition1] | key2[condition2] | key3[condition3]"/>

where only one of the conditions is actually true.

There's also the dynamic name trick:

<xsl:sort select="*[name()=$param]"/>

and of course if the worst comes to the worst you can always write

<xsl:choose>
<xsl:when test="condition1">
  <xsl:for-each select="$x">
    <xsl:sort select="sort-key-1"/>
    <xsl:call-template name="the-work"/>
  </xsl:for-each>
</xsl:when>
<xsl:when test="condition2">
  <xsl:for-each select="$x">
    <xsl:sort select="sort-key-2"/>
    <xsl:call-template name="the-work"/>
  </xsl:for-each>
</xsl:when>
...

XSLT 2.0 also introduces a sort() function that takes a named sort key as a parameter, which can be determined at run-time:

<xsl:for-each 
         select="sort($x, if (condition1) then 'sortkey1' 
                          else
                              'sortkey2')">

2.

Sort and xt:node-set

Sebastian Rahtz


<foo>
 <bar id="1" links="a b c"/>
 <bar id="2" links="b d d e f"/>
 <bar id="3" links="b"/>
 <bar id="4" links="c a"/>
 <bar id="5" links="g j"/>
 <bar id="6" links="a f"/>
</foo>

and I want make a sorted catalogue of the bits of the "links" attribute, showing the <bar> each is found in.

I append my stylesheet, using XT's node-set extension. I run over the <bar> elements, splitting the "links" value, and building a new node-set. I then sort that, make a new node-set, and step through it finding the different 'a', 'b', 'c' etc.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        version="1.0"
        xmlns:xt="http://www.jclark.com/xt"
        extension-element-prefixes="xt">

<xsl:template match="foo">
<!-- store in a variable the inverted list of <bar> elements -->
<xsl:variable name="results">
  <xsl:for-each select="bar">
    <xsl:call-template name="searchlist">
      <xsl:with-param name="list" 
	select="concat(@links,' ')"/>
    </xsl:call-template>
  </xsl:for-each>
</xsl:variable>

<!-- now convert the list to a node-set, sort, and store again -->
<xsl:variable name="sorted">
  <xsl:for-each select="xt:node-set($results)/bar">
   <xsl:sort select="@id"/>
   <xsl:sort select="@parent"/>
     <bar id="{@id}" parent="{@parent}"/>
  </xsl:for-each>
</xsl:variable>

<!-- now convert that to a node-set and step through it,
     looking for the first occurrence of each id -->


<xsl:for-each select="xt:node-set($sorted)/bar">
  <xsl:variable name="c" select="@id"/>
  <xsl:if test="not(preceding-sibling::bar[$c=@id])">
Link: <xsl:value-of select="@id"/> 
- ----------
 <xsl:apply-templates select="." mode="final"/> 
 <xsl:apply-templates 
  select="following-sibling::bar[$c=@id]" mode="final"/>
- -----------
  </xsl:if>
</xsl:for-each>
</xsl:template>

<xsl:template match="bar" mode="final">
  <xsl:value-of 
  select="@parent"/><xsl:text> / </xsl:text>
</xsl:template>

<xsl:template name="searchlist">
<!-- 
  split up the list by space, and for each value
  make a new <bar> element, and then recurse to get another
  value
- -->
 <xsl:param name="list"/>
 <xsl:if test="not($list = '')">
  <bar id="{substring-before($list,' ')}" 
	parent="{@id}"/>
  <xsl:call-template name="searchlist">
    <xsl:with-param name="list" 
        select="substring-after($list,' ')"/>
  </xsl:call-template>
 </xsl:if>
</xsl:template>

</xsl:stylesheet>

            

3.

Group sort and number

Jarno Elovirta

Suppose you have the following XML:

	<animals>
		<animal type="dog" name="Fido">
		<animal type="cat" name="Kitty">
		<animal type="bird" name="Tweety">
		<animal type="horse" name="Trigger">
		<animal type="cat" name="Tom">
		<animal type="pig" name="Porky">
		<animal type="fish" name="Charlie">
		<animal type="pig" name="Babe">
		<animal type="cow" name="Elsie">
		<animal type="cat" name="Puss">
	<animals>

and you want to transform this into the following output:

	Here are my pets:
	1.  dog (Fido)
	2.  cat (Kitty)
	3.  cat (Tom)
	4.  cat (Puss)
	5.  bird (Tweety)
	6.  horse (Trigger)
	7.  pig (Porky)
	8.  pig (Babe)
	9.  fish (Charlie)
	10.  cow (Elsie)

Notice that the elements are output in physical order, except when there is more than one of the same type, in which case the duplicates are grouped together with the first occurrence.

This does it, but i'm quite sure there are better ways of doing it.

[c:\temp]type test.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<animals>
  <animal type="dog" name="Fido" />
  <animal type="cat" name="Kitty" />
  <animal type="bird" name="Tweety" />
  <animal type="horse" name="Trigger" />
  <animal type="cat" name="Tom" />
  <animal type="pig" name="Porky" />
  <animal type="fish" name="Charlie" />
  <animal type="pig" name="Babe" />
  <animal type="cow" name="Elsie" />
  <animal type="cat" name="Puss" />
</animals>
[c:\temp]type test.xsl
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:output method="text" />

<xsl:key name="sort" match="animal" use="@type"/>
<xsl:variable 
   name="list" 
   select="animals/animal[generate-id(.) =
   generate-id(key('sort', @type))]" />

<xsl:template match="/">
  <xsl:call-template name="process-list">
    <xsl:with-param name="index" select="1" />
    <xsl:with-param name="position" select="1" />
    <xsl:with-param name="counter" select="0" />
  </xsl:call-template>
</xsl:template>

<xsl:template name="process-list">
  <xsl:param name="index" />
  <xsl:param name="position" />
  <xsl:param name="counter" />
  <xsl:choose>
    <!-- process new @type -->
    <xsl:when 
   test="$counter = count($list[@type = 
   $list[$index]/@type]) - 1">
      <xsl:apply-templates select="$list[$index]">
        <xsl:with-param name="position" select="$position" />
      </xsl:apply-templates>
      <xsl:call-template name="process-list">
        <xsl:with-param name="index" select="$index" />
        <xsl:with-param name="position" select="$position + 1" />
        <xsl:with-param name="counter" select="1" />
      </xsl:call-template>
    </xsl:when>
    <!-- process the rest of the current @type -->
    <xsl:otherwise>
      <xsl:apply-templates select="animals/animal[@type =
$list[$index]/@type][$counter + 1]">
        <xsl:with-param name="position" select="$position" />
      </xsl:apply-templates>
      <xsl:choose>
        <!-- goto next @type -->
        <xsl:when test="$counter = count(animals/animal[@type =
$list[$index]/@type])">
          <!-- test if there are more @types -->
          <xsl:if test="$index <= count($list)">
            <xsl:call-template name="process-list">
              <xsl:with-param name="index" select="$index + 1" />
              <xsl:with-param name="position" select="$position" />
              <xsl:with-param name="counter" select="0" />
            </xsl:call-template>
          </xsl:if>
        </xsl:when>
        <!-- goto next in this @type -->
        <xsl:otherwise>
          <xsl:call-template name="process-list">
            <xsl:with-param name="index" select="$index" />
            <xsl:with-param name="position" select="$position + 1" />
            <xsl:with-param name="counter" select="$counter + 1" />
          </xsl:call-template>
        </xsl:otherwise>
      </xsl:choose>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

<xsl:template match="animal">
  <xsl:param name="position" />
  <xsl:value-of select="$position" />
  <xsl:text>.  </xsl:text>
  <xsl:value-of select="@type" />
  <xsl:text> (</xsl:text>
  <xsl:value-of select="@name" />
  <xsl:text>)
</xsl:text>
</xsl:template>

</xsl:stylesheet>

To give the following output

[c:\temp]saxon test.xml test.xsl
1.  dog (Fido)
2.  cat (Kitty)
3.  cat (Tom)
4.  cat (Puss)
5.  bird (Tweety)
6.  horse (Trigger)
7.  pig (Porky)
8.  pig (Babe)
9.  fish (Charlie)
10.  cow (Elsie)

4.

Grouping on an element

David Carlisle


From the following XML I need to group  on ITEMTYPE
and sort on NAME



- -----------------------------  Sample XML ----------------------
<?xml version="1.0"?>

<TOP>
    <LEVEL1>
        <LEVEL2>
            <GROUP>

                <ITEM>
                    <NAME>Name1</NAME>
                    <INFO>
                        <INFOTYPE>
                            <ID1>001</ID1>
                            <ID2>001</ID2>
                            <ITEMTYPE>TYPE1</ITEMTYPE>
                        </INFOTYPE>
                    </INFO>
                </ITEM>

                <ITEM>
                    <NAME>Name2</NAME>
                    <INFO>
                        <INFOTYPE>
                            <ID1>002</ID1>
                            <ID2>002</ID2>
                            <ITEMTYPE>TYPE1</ITEMTYPE>
                        </INFOTYPE>
                    </INFO>
                </ITEM>

                <ITEM>
                    <NAME>Name3</NAME>
                    <INFO>
                        <INFOTYPE>
                            <ID1>003</ID1>
                            <ID2>003</ID2>
                            <ITEMTYPE>TYPE2</ITEMTYPE>
                        </INFOTYPE>
                    </INFO>
                </ITEM>

                <ITEM>
                    <NAME>Name4</NAME>
                    <INFO>
                        <INFOTYPE>
                            <ID1>004</ID1>
                            <ID2>004</ID2>
                            <ITEMTYPE>TYPE2</ITEMTYPE>
                        </INFOTYPE>
                    </INFO>
                </ITEM>

            </GROUP>
        </LEVEL2>
    </LEVEL1>
</TOP>





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

 <xsl:output method="html"/>

 <xsl:template match="/">
     <HTML>

     <BODY>

      

       <!-- for each item -->
       <xsl:for-each 
	select="//ITEMTYPE[not(. = following::ITEMTYPE)]">
          <xsl:sort/>
           <H1><xsl:value-of select="."/></H1>
         <xsl:for-each 
	select="//ITEM[INFO/INFOTYPE/ITEMTYPE=current()]">
          <xsl:sort select="NAME"/>
           <P><xsl:value-of 
	select="NAME"/></P>
        
       </xsl:for-each>
       </xsl:for-each>

     </BODY>
     </HTML>
 </xsl:template>

</xsl:stylesheet>


Produces output

<HTML>
<BODY>
<H1>TYPE1</H1>
<P>Name1</P>
<P>Name2</P>
<H1>TYPE2</H1>
<P>Name3</P>
<P>Name4</P>
</BODY>
</HTML>

            

5.

Sorting and grouping refinement

David Carlisle

generate-id(.) = generate-id(key('tid',.)[1])

tests if the current node is the first node returned by the key and

generate-id(.) = generate-id(key('tid',.))

is the same due to `take first node in node set' semantics

but

count(.|key('tid',.))=1

tests that the key only returns one node, and that that is the current one.

You want

count(.|key('tid',.)[1])=1
which is equivalent to the generate-id tests.

Of course, it's only equivalent in the case that you know that the key returns something, otherwise if the key returns the empty set then the above will always be true as .|key('tid',.)[1] will be .

In general the test "is the current node the first node in the node set x" is either

"generate-id(.) = generate-id($x[1])"  (the [1] is optional here)

or

"count(.|$x[1])=count($x[1])"

or

"$x and count(.|$x[1])= 1"


6.

Sorting and grouping

Nikolai Grigoriev


> I want to group consecutive days with the same hours together, and just
> print the first and last day in each group.
>
> I also want to ignore the 'Holidays' day. I put it in there because I
> can't use a solution that assumes Sunday's hours aren't followed by
> something that could be the same.

This can be achieved by recursion: you call a template recursively until there's no more following-siblings that have the same text (passed as a param), like in the stylesheet below:

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

<xsl:output method="html" version="4.0"/>

<!-- Root template: just create a table. -->
<xsl:template match="Hours">
  <table><xsl:apply-templates/></table>
</xsl:template>

<!-- Exclude holidays from processing -->
<xsl:template match="Holidays" priority="2"/>

<!-- Single days, except for holidays. -->
<!-- A modeless template creates the row -->
<xsl:template match="Hours/*">
  <xsl:variable name="hours" select="text()"/>
  <xsl:if
      test="not(preceding-sibling::*[not(self::Holidays)][1][text()=$hours])">
    <tr>
      <td>
        <xsl:value-of select="name()"/>
        <xsl:apply-templates mode="end"

select="following-sibling::*[not(self::Holidays)][1][text()=$hours]"/>
      </td>
      <td><xsl:value-of select="."/></td>
    </tr>
  </xsl:if>
</xsl:template>

<!-- A day closes the period if there's no better candidate -->
<xsl:template match="Hours/*" mode="end">
  <xsl:variable name="hours" select="text()"/>
  <xsl:choose>
    <xsl:when
        test="following-sibling::*[not(self::Holidays)][1][text()=$hours]">
      <xsl:apply-templates mode="end"
          select="following-sibling::*[not(self::Holidays)][1]"/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:text> - </xsl:text><xsl:value-of select="name()"/>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

</xsl:stylesheet>

7.

Add new element every 7th time

Jeni Tennison

>I'm trying to generage output into wml.  When a return from database is more
>than 7, new element is added and also insert a new card.  
    

You say that you've figured out how to split the titles into cards, but don't know how to insert a 'More' choice when you need to. How you do it exactly is dependent on how you're dividing your input up into cards in the first place. Basically, you need to check whether there is another 'title' element to process after the last one you processed. If there is, then you want to output a 'More' choice; if not, then you don't.

Here's how I've approached the problem - hopefully the solution will map onto yours fairly easily.

The first thing I did is create a parameter to hold the magic number 7, just in case you want to change it in the future:

<xsl:param name="group-size" select="'7'" />

When you have a grouping problem, you need to: (a) identify the first thing in each group and (b) identify all the other things in the group, based on knowing the first one

When you're grouping into groups of a certain size based on position, you can find the first things in each group by looking at the position() of the thing mod the size of the group. The first in each group will have a value of '1', the second a value of '2' and so on. You want to apply templates to only these things, in your case 'title' elements:

<xsl:template match="book">
  <wml>
    <xsl:apply-templates select="title[(position() mod $group-size) = 1]" />
  </wml>
</xsl:template>

Now, a 'title'-matching template will only be processed on the first title in each group. When the 'title'-matching is processed, the current node list is comprised of only those title elements that are first in each group: the position() of the title element within this list gives you the number of the card to use.

The group consists of the current title (the first in the group) plus the next 6 titles, in other words, the following sibling titles whose position is less than the size of the group. You have a choice for each of those. Then, if there is a 7th title element following the first in the group, then that title element is going to be in a new card - in that case, you need a 'More' choice.

<xsl:template match="title">
  <xsl:variable name="card-no" select="position()" />
  <card id="{$card-no}">
    <select>
      <xsl:for-each select=". | following-sibling::title[position() &lt;
$group-size]">
        <choice><xsl:value-of select="." /></choice>
      </xsl:for-each>
      <xsl:if test="following-sibling::title[position() = $group-size]">
        <choice onpick="#{$card-no + 1}">More</choice>
      </xsl:if>
    </select>
  </card>
</xsl:template>

These templates have been tested and work in SAXON given your output.

8.

Sorting and Grouping example

David Carlisle



<html>
<h1 text="h1text">
<p>ptextptext</p>
<p>ptextptext</p>
<h2 text="h2texth2text">
<p>ptextptext</p>
<p>ptextptext</p>
<p>ptextptext</p>
</h2>
<h2 text="h2texth2text">
<p>ptextptext</p>
<p>ptextptext</p>
<list>
<li>litextlitext</li>
<li>litextlitext</li>
<li>litextlitext</li>
</list>
</h2>
<h2 text="h2texth2text">
<p>ptextptext</p>
<p>ptextptext</p>
</h2>
</h1>
</html>
    
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform">


<xsl:template match="html">
  <xsl:apply-templates select="*[1]"/>
</xsl:template>

<xsl:template match="h1">
 <h1 text="{.}">
  <xsl:apply-templates
  mode="h1"
  select="following-sibling::*[1][not(self::h1)]"/>
 </h1>
 <xsl:apply-templates select=
  "following-sibling::h1[1]"/>
</xsl:template>

<xsl:template mode="h1" match="h2">
 <h2 text="{.}">
  <xsl:apply-templates
  mode="h2"
  select="following-sibling::*[1][not(self::h2)]"/>
 </h2>
 <xsl:apply-templates 
   mode="h1"
   select= "following-sibling::h2[1]"/>
</xsl:template>


<xsl:template mode="h1" match="*">
 <xsl:copy>
 <xsl:apply-templates/>
 </xsl:copy>
<xsl:if test="following-sibling::*[1][not(self::h1)]">
<xsl:apply-templates mode="h1" select="following-sibling::*[1]"/>
</xsl:if>
</xsl:template>


<xsl:template mode="h2" match="li">
 <list>
<xsl:apply-templates mode="li" select="."/>
</list>
</xsl:template>

<xsl:template mode="li" match="li">
 <xsl:copy-of select="."/>
<xsl:apply-templates mode="li" 
      select="following-sibling::*[1][self::li]"/>
</xsl:template>


<xsl:template mode="h2" match="*">
 <xsl:copy>
 <xsl:apply-templates/>
 </xsl:copy>
<xsl:if test="following-sibling::*[1][not(self::h2)]">
<xsl:apply-templates mode="h2" select="following-sibling::*[1]"/>
</xsl:if>
</xsl:template>

</xsl:stylesheet>

9.

How to number on a grouped and sorted set?

Jeni Tennison

> I'm working with a group and sort set defined like this and just want to
> write the order (number - 1,2,3,etc.) into the last element called <rank>.
> How do I do that?
	

You have a xsl:for-each that iterates over the set of results that are the first with a particular cuicode. Within that xsl:for-each, the current node set is that list of results, so giving the position() of the particular 'result' element within that set will number them sequentially, which I think is what you were after. i.e.:

  <rank><xsl:value-of select="position()" /></rank>

xsl:number is best used for numbering items according to their position in the source tree rather than the result tree. You *could* use it in this situation, by changing the counted nodes to be the result elements that are first with a particular cuicode, but that's a bit complicated when you can use position() instead.

I hope you don't mind me just commenting on another bit of the stylesheet: it's probably what you were after, but just to make sure: the first sort you're doing is on:

  sum(key('g', cuicode)/bitmask)

This will sum the values of *all* the bitmask children of the results with the same cuicode. I just wanted to check that this was what you were after, rather than the sum of the bitmask children of the result elements that are actually being sorted.

The other sorts will sort on the value of (a) the count and (b) the offset elements that appear first as children of the results with the same cuicode. That means that if the first result element with a particular cuicode doesn't have a 'count' or 'offset' child, then it will try to sort on the 'count' or 'offset' of the next result element with the same cuicode, unless it doesn't have one, in which case it'll move onto the next and so on.

It's very possible that this is the behaviour you're after, but it's equally likely that you actually were only interested in the values for the first result, or that you can guarantee that the first result has 'count' or 'offset' children. If that's the case, rather than refer to the key again, just use:

  <xsl:sort data-type="number" select="count" order="descending" />
  <xsl:sort data-type="number" select="offset" order="descending" />

10.

Multiple output and grouping

Jeni Tennison

I'm trying to generate a set of HTML pages from a single XML document. Basically, my problem has to do with sorting and grouping. The XML source looks like this:

<author>
  <name>...</name>
  <records>
    <record>...</record>
    ...
  </records>
</author>
...

That is, I have a list of authors and a list of records associated with each author. What I want to do is sorting the list by author and grouping the authors (with their records) alphabetically in different HTML files

OK. First you need to be able to, given a letter, find out which authors to output. You can find the authors whose names begin with a certain letter using starts-with(). For example:

  /authors/author[starts-with(name, $letter)]

In other words, select the author elements whose name child element starts with $letter.

Or you can set up a key that lets you index into the list of authors according to the first letter of their name:

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

In other words, set up a key space called 'authors' that indexes into any author elements according to the first character of their name. You can then access all authors whose names start with a certain letter using:

  key('authors', $letter)

This is probably more efficient, especially as you'll be retrieving them 26 times, and especially if you have a long list of authors (as I guess is likely?)

So, given a letter, you can collect the nodes that represent the authors together and output whatever you want from them. When you iterate over them using xsl:for-each or apply templates to then using xsl:apply-templates you can sort them according to their name using xsl:sort:

  <xsl:sort select="name" />

You need to have the letter passed into this general template as a parameter. Something like:

<xsl:template name="output-authors-by-letter">
  <xsl:param name="letter" select="'A'" />
  <saxon:output file="{$letter}.html">
    <html>
      <head>
        <title>Authors starting with <xsl:value-of select="$letter"
        /></title>
      </head>
      <body>
        <h1><xsl:value-of select="$letter" /></h1>
        <xsl:for-each select="key('authors', $letter)">
          <xsl:sort select="name" />
          <h2><xsl:value-of select="name" /></h2>
          <xsl:apply-templates select="records" />
        </xsl:for-each>
      </body>
    </html>
  </saxon:output>
</xsl:template>

So, how to get the letter to be passed into the template. First, you need to know your alphabet, so set a variable up to hold it:

<xsl:variable name="alphabet" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'" />

Now you need something to work its way through that string. In XSLT you can do this by recursion: have a template that takes a string (starting with the full alphabet), takes the first letter and calls the above template with it, and then calls itself on the rest of that string:

<xsl:template name="output-authors">
  <xsl:param name="alphabet" select="$alphabet" />
  <xsl:if test="$alphabet">
    <xsl:call-template name="output-authors-by-letter">
      <xsl:with-param name="letter"
                      select="substring($alphabet, 1, 1)" />
    </xsl:call-template>
    <xsl:call-template name="output-authors">
      <xsl:with-param name="alphabet"
                      select="substring($alphabet, 2)" />
    </xsl:call-template>
  </xsl:if>
</xsl:template>

You can also do this using xsl:for-each and the Piez Technique (see, Wendell ;) Take a node set of 26 nodes (you probably have that many elements in your document easily) and iterate over them, using the position() of the node to tell which letter to take from the alphabet:

<xsl:template name="output-authors">
  <xsl:for-each select="//*[position() &lt;= 26]">
    <xsl:call-template name="output-authors-by-letter">
      <xsl:with-param name="letter"
                      select="substring($alphabet, position(), 1)" />
    </xsl:call-template>
  </xsl:for-each>
</xsl:template>

11.

sort and grouping on 2 different attributes

Americo Albuquerque

Expanded Question

I have the following simplified xml

<root>
  <rec A="0" B="1">1</rec>
  <rec A="0" B="3">2</rec>
  <rec A="0" B="3">3</rec>
  <rec A="0" B="3">4</rec>
  <rec A="1" B="1">5</rec>
  <rec A="1" B="1">6</rec>
  <rec A="2" B="1">7</rec>
  <rec A="3" B="1">8</rec>
</root>

each node has 2 attributes ( A and B ). When A="0" then B should be used for grouping When A != "0" then A should be used for grouping. Sorting should be done on A and B ( my simplified example xml is already sorted on A and B)

the result should be such that when a group is changed it displays the subtitle

title-1
  <rec A="0" B="1">1</rec>
title-2
  <rec A="0" B="3">2</rec>
  <rec A="0" B="3">3</rec>
  <rec A="0" B="3">4</rec>
title-3
  <rec A="1" B="1">5</rec>
  <rec A="1" B="1">6</rec>
title-4
  <rec A="2" B="1">7</rec>
title-5
  <rec A="3" B="1">8</rec>

I find it hard to find a simple but waterproof solution for grouping on the 2 different attributes.

Answer

To group on 2 attributes you'll have to use concat like this:

<xsl:key name="group" match="node" use="concat(@attr1,' ',@attr2)"/>

When applying the key you'll do the same:

select="key('group',concat(@attr1,' ',@attr2)"

Related to your problem, this templates do what you ask, you'll have to change them to your needs.

 <xsl:key name="recs" match="rec" use="@A"/>
 <xsl:key name="recs" match="rec" use="concat(@A,' ',@B)"/>
 
 <xsl:template match="root">
  <xsl:apply-templates
select="rec[@A='0'][generate-id()=generate-id(key('recs',concat(@A,'
',@B)))]|rec[not(@A='0')][generate-id()=generate-id(key('recs',@A))]">
    <xsl:sort select="@A" data-type="number"/>
    <xsl:sort select="@B" data-type="number"/>
   </xsl:apply-templates>
 </xsl:template>
 
 <xsl:template match="rec">
  <xsl:text>titulo-</xsl:text>
  <xsl:value-of select="position()"/>
  <xsl:text>&#10;</xsl:text><!-- or whatever you like as title -->
  <xsl:choose> <!-- choose whet key to apply -->
   <xsl:when test="@A=0">
   <!-- apply to A='0', so group also by @B -->
   <xsl:apply-templates select="key('recs',concat(@A,' ',@B))"
mode="table">
    <xsl:sort select="@A" data-type="number"/>
    <xsl:sort select="@B" data-type="number"/>
   </xsl:apply-templates>
   </xsl:when>
   <xsl:otherwise>
   <!-- apply to A<>'0', group just by @A -->
   <xsl:apply-templates select="key('recs',@A)" mode="table">
    <xsl:sort select="@A" data-type="number"/>
    <xsl:sort select="@B" data-type="number"/>
   </xsl:apply-templates>
   </xsl:otherwise>
  </xsl:choose>
  <xsl:text>&#10;</xsl:text>
 </xsl:template>
 
 <xsl:template match="rec" mode="table">
  <!-- replace this for yours -->
  <!-- here I'm just displaying the matched nodes -->
  <xsl:text> &lt;rec</xsl:text>
  <xsl:apply-templates select="@*" mode="showattribs"/>
  <xsl:text>&gt;</xsl:text>
  <xsl:value-of select="."/>
  <xsl:text>&lt;/rec&gt;&#10;</xsl:text>
 </xsl:template>
 
 <xsl:template match="@*" mode="showattribs">
  <xsl:text> </xsl:text>
  <xsl:value-of select="name()"/>
  <xsl:text>="</xsl:text>
  <xsl:value-of select="."/>
  <xsl:text>"</xsl:text>
 </xsl:template>

12.

Group and sort by group element occurrences

Mukul Gandhi



> I am currently using an XSLT stylesheet to transform
> one type of XML into
> another.  The first type looks like this:

> <FruitList>
>  <Fruit ID="5" KEY="apple" VALUE="true">
>  <Fruit ID="5" KEY="orange" VALUE="false">
>  <Fruit ID="4" KEY="orange" VALUE="false">
>  <Fruit ID="5" KEY="banana" VALUE="false">
>  <Fruit ID="4" KEY="pineapple" VALUE="false">
>  <Fruit ID="13" KEY="orange" VALUE="false">
>  <Fruit ID="13" KEY="watermelon" VALUE="true">
>  <Fruit ID="4" KEY="kiwi" VALUE="false">
>  <Fruit ID="4" KEY="grapefruit" VALUE="true">
>  <Fruit ID="13" KEY="papaya" VALUE="false">
>  <Fruit ID="13" KEY="honeydew" VALUE="true">
> </FruitList>

> I'd like to write a stylesheet to transform it as
> follows:

> <FruitList>
>  <Fruit ID="5">
>  <Property KEY="apple" VALUE="true">
>  <Property KEY="orange" VALUE="false">
>  <Property KEY="banana" VALUE="false">
>  </Fruit>
>  <Fruit ID="4">
>  <Property KEY="orange" VALUE="false">
>  <Property KEY="pineapple" VALUE="false">
>  <Property KEY="kiwi" VALUE="false">
>  <Property KEY="grapefruit" VALUE="true">
>  </Fruit>
>  <Fruit ID="13">
>  <Property KEY="orange" VALUE="false">
>  <Property KEY="watermelon" VALUE="true">
>  <Property KEY="papaya" VALUE="false">
>  <Property KEY="honeydew" VALUE="true">
>  </Fruit>
> </FruitList>

Use Muenchian method for grouping. Below is the complete XSL --

<?xml version="1.0" encoding="UTF-8"?>
<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:key name="x" match="Fruit" use="@ID"/>
   
   <xsl:template match="/FruitList">
     <Fruitlist>
       <xsl:for-each select="Fruit">
	 <xsl:if test="generate-id(.) = generate-id(key('x',
@ID)[1])">
	 <Fruit ID="{@ID}">
	   <xsl:for-each select="key('x', @ID)">
              <Property KEY="{@KEY}" VALUE="{@VALUE}">
                                   
              </Property>
           </xsl:for-each>
         </Fruit>    
	</xsl:if>
      </xsl:for-each>
   </Fruitlist>
</xsl:template>
</xsl:stylesheet>

13.

Sorting and grouping

Thomas Stone

Subject: Meunch away on this

I am looking for feedback on my solution to a very old topic... sorting and grouping in XSLT version 1.0. I am using Mozilla Firefox version 1.0.7 to read an XML document referencing an XSLT stylesheet to produce a simple HTML table. The desired data is, oddly enough, the element names of the XML document.

Any source XML document will do. It will need to have a processor directive pointing to the below sample stylesheet.

To list all the elements of that document, the stylesheet would be as follows:

<?xml version "1.0" encoding "ISO-8859-1"?>
<xsl:stylesheet version "1.0" xmlns:xsl "http://www.w3.org/1999/XSL/Transform">
 <xsl:template match "/">
   <html><head><title>Tags List</title></head>
     <body><table border "1">
         <tr><th>Tag Name</th></tr>

         <xsl:apply-templates select "//*" mode "all">
           <xsl:sort select "name()"/>
         </xsl:apply-templates>
     </table></body>
   </html>
 </xsl:template>


 <xsl:template match "*" mode "all">
   <tr>
     <td><xsl:value-of select "name()"/></td>
   </tr>
 </xsl:template>
</xsl:stylesheet>

This list is sorted and shows all the data I need, but the question that I've seen posts on back to 1999 is how to make this a sorted unique list. My hat's still off to Steve Meunch for the key value solution. I don't even want to try to figure out a faster way to uniquely sort a list. I was only interested in finding a less, if you'll pardon me, convoluted way to do it so it could be implemented without a long explanation. Though, again, thanks to Jeni for her site.

Here is what I came up with that seems pretty straight forward. Using t he Position() function within the sorted list, only output the first position. This will always get the first entry in alphabetical order. Select sorted all elements that are not the same name as the first and recurse to the same procedure, thus displaying only the second entry in alphabetical order. Append each entry to a delimited string array and use a Contains() test to eliminate duplicates from the next selection list.

         <xsl:apply-templates select "//*" mode "unique">
           <xsl:sort select "name()"/>
           <xsl:with-param name "code_list" select "';'"/>
         </xsl:apply-templates>

 <xsl:template match "*" mode "unique">
   <xsl:param name "code_list"/>

   <xsl:if test "position() 1">

     <xsl:variable name "ent_name" select "name()"/>

     <tr>
       <td><xsl:value-of select "$ent_name"/></td>
     </tr>

     <xsl:variable name "new_list" select "concat($code_list,
     concat($=ent_name, ';'))"/>

     <xsl:apply-templates select "//*[contains($new_list, concat(';',
     concat(name(), ';'))) false()]" mode "unique">
       <xsl:sort select "name()"/>
       <xsl:with-param name "code_list" select "$new_list"/>
     </xsl:apply-templates>
   </xsl:if>
 </xsl:template>

This gives me a unique sorted list of all entities in the document. I prefer to have them enumerated.

         <xsl:apply-templates select "//*" mode "summary">
           <xsl:sort select "name()"/>
           <xsl:with-param name "code_list" select "';'"/>
           <xsl:with-param name "seq_counter" select "1"/>
         </xsl:apply-templates>

 <xsl:template match "*" mode "summary">
   <xsl:param name "code_list"/>
   <xsl:param name "seq_counter"/>

   <xsl:if test "position() 1">

     <xsl:variable name "ent_name" select "name()"/>

     <tr>
       <td><xsl:value-of select "$seq_counter"/></td>
         <td><xsl:value-of select "$ent_name"/></td>
     </tr>

     <xsl:variable name "new_list" select "concat($code_list,
     concat($ent_name, ';'))"/>

     <xsl:apply-templates select "//*[contains($new_list, concat(';',
     concat(name(), ';'))) false()]" mode "summary">
       <xsl:sort select "name()"/>
       <xsl:with-param name "code_list" select "$new_list"/>
       <xsl:with-param name "seq_counter" select "$seq_counter+1"/>
     </xsl:apply-templates>
   </xsl:if>
 </xsl:template>

From this structure, I can group by placing a correlated sub-query <apply-templates> where the <tr> output is. The uniqueness test can be applied just as directly to character data or attributes.