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() <
$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() <= 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> </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> </xsl:text>
</xsl:template>
<xsl:template match="rec" mode="table">
<!-- replace this for yours -->
<!-- here I'm just displaying the matched nodes -->
<xsl:text> <rec</xsl:text>
<xsl:apply-templates select="@*" mode="showattribs"/>
<xsl:text>></xsl:text>
<xsl:value-of select="."/>
<xsl:text></rec> </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. |