Forum Moderators: open
I've managed to sort my events by days. Since there's a fixed number of days, it's pretty easy to sort them (Sunday, Monday, Tuesday, Wednesday, Thursday, Friday and Saturday. If there are no events on a given day, I simply don't display that day).
The next step is to sort them by location. Specifically, the town in which they are located.
This is not so simple, as there are dozens of towns, and we don't have a nice, simple layout of seven distinct categories.
I need to first extract all the towns, and then iterate through them.
I'll work out some simulacrum in weetest, and post what I get here.
[edited by: cmarshall at 2:36 am (utc) on April 1, 2007]
wee_test.xml now looks like this:
<?xml version="1.0" encoding="ISO-8859-1"?>
<weetest xmlns="http://www.example.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.example.com file:/Users/cmarshall/Desktop/XMLTest/wee_test/wee_test.xsd">
<elementwrapper id="5">
<data_cat>4</data_cat>
<data_name>Test (cat 4)</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="1">
<data_cat>1</data_cat>
<data_duration>01:05:00</data_duration>
<data_name>Test (cat 1)</data_name>

</elementwrapper>
<elementwrapper id="3">
<data_cat>2</data_cat>
<data_duration>21:35:00</data_duration>
<data_name>Test (cat 2)</data_name>
</elementwrapper>
<elementwrapper id="4">
<data_cat>2</data_cat>
<data_name>Test (cat 2)</data_name>

</elementwrapper>
<elementwrapper id="6">
<data_cat>4</data_cat>
<data_duration>11:01:20</data_duration>
<data_name>Test (cat 4)</data_name>
</elementwrapper>
<elementwrapper id="2">
<data_cat>1</data_cat>
<data_name>Test (cat 1)</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="7">
<data_cat>2</data_cat>
<data_duration>10:00:01</data_duration>
<data_name>Test (cat 2)</data_name>
</elementwrapper>
<elementwrapper id="8">
<data_cat>1</data_cat>
<data_duration>10:01:00</data_duration>
<data_name>Test (cat 1)</data_name>
</elementwrapper>
<elementwrapper id="9">
<data_cat>2</data_cat>
<data_name>Test (cat 2)</data_name>

</elementwrapper>
<elementwrapper id="0">
<data_cat>4</data_cat>
<data_name>Test (cat 4)</data_name>
</elementwrapper>
</weetest>
Now, I want to go back to listing the categories in separate groups, like I did before, but this time, I'm going about it differently.
Instead of already knowing how many categories I have, and iterating over them, I want to search the file, and extract the number of categories. If I do this, I'll wind up with a set that doesn't have category 3, and will have category 4. This is a pretty good analogy for what I'm doing in the "real" application.
I'm not sure how I'll go about this...
I know XPath can reference the previous node in a node-set, but I just can't find a $#@! example of it!
I get dozens of hits in Google, and every single one is worthless to me!
This is the problem I've been having learning XML and XSLT all along.
I've never encountered anything like this before.
I'll get it...
I am filtering and sorting for all nodes that have a
data_cat element (in this case, all of them), and I am sorting the resulting node-set. I iterate over them. This keeps me from worrying about the missing Category 3, and it finds Category 4: The current XML (wee_test.xml):
<?xml version="1.0" encoding="ISO-8859-1"?>
<weetest xmlns="http://www.example.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.example.com file:/Users/cmarshall/Desktop/XMLTest/wee_test/wee_test.xsd">
<elementwrapper id="5">
<data_cat>4</data_cat>
<data_name>Test (cat 4)</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="1">
<data_cat>1</data_cat>
<data_duration>01:05:00</data_duration>
<data_name>Test (cat 1)</data_name>

</elementwrapper>
<elementwrapper id="3">
<data_cat>2</data_cat>
<data_duration>21:35:00</data_duration>
<data_name>Test (cat 2)</data_name>
</elementwrapper>
<elementwrapper id="4">
<data_cat>2</data_cat>
<data_name>Test (cat 2)</data_name>

</elementwrapper>
<elementwrapper id="6">
<data_cat>4</data_cat>
<data_duration>11:01:20</data_duration>
<data_name>Test (cat 4)</data_name>
</elementwrapper>
<elementwrapper id="2">
<data_cat>1</data_cat>
<data_name>Test (cat 1)</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="7">
<data_cat>2</data_cat>
<data_duration>10:00:01</data_duration>
<data_name>Test (cat 2)</data_name>
</elementwrapper>
<elementwrapper id="8">
<data_cat>1</data_cat>
<data_duration>10:01:00</data_duration>
<data_name>Test (cat 1)</data_name>
</elementwrapper>
<elementwrapper id="9">
<data_cat>2</data_cat>
<data_name>Test (cat 2)</data_name>

</elementwrapper>
<elementwrapper id="0">
<data_cat>4</data_cat>
<data_name>Test (cat 4)</data_name>
<data_uri/>
</elementwrapper>
</weetest>
The current schema (wee_test.xsd):
<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.example.com"
elementFormDefault="qualified">
<xs:element name="weetest">
<xs:complexType>
<xs:sequence maxOccurs="unbounded">
<xs:element name="elementwrapper">
<xs:complexType>
<xs:sequence>
<xs:element name="data_cat" type="xs:integer"/>
<xs:element name="data_duration" minOccurs="0" type="xs:time"/>
<xs:element name="data_name" type="xs:string"/>
<xs:element minOccurs="0" name="data_uri" type="xs:anyURI"/>
</xs:sequence>
<xs:attribute name="id" type="xs:integer" />
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
The current XSLT stylesheet (wee_test.xsl):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:wt="http://www.example.com" exclude-result-prefixes="wt">
<xsl:output indent="yes" method="html" version="4.01" doctype-system="http://www.w3c.org/tr/html4/strict.dtd" doctype-public="-//W3c//DTD HTML 4.01//EN"/><xsl:template match="/">
<html>
<head>
<title>XML Test</title>
</head>
<body>
<xsl:variable name="cats" select="wt:weetest/wt:elementwrapper[wt:data_cat!='']"/>
<xsl:for-each select="$cats">
<xsl:sort select="wt:data_cat"/>
<h1>Category <xsl:value-of select="wt:data_cat"/></h1>
<xsl:call-template name="render_one"/>
</xsl:for-each>
</body>
</html>
</xsl:template><xsl:template name="render_one">
<xsl:element name="div">
<xsl:attribute name="class">
<xsl:text>alt_</xsl:text>
<xsl:number value="position() mod 2" format="1"/>
</xsl:attribute>
<xsl:call-template name="display_duration">
<xsl:with-param name="duration" select="wt:data_duration"/>
</xsl:call-template>
<div class="display">
<xsl:choose>
<xsl:when test="wt:data_uri!= ''">
<xsl:element name="a">
<xsl:attribute name="href">
<xsl:value-of disable-output-escaping="yes" select="wt:data_uri"/>
</xsl:attribute>
<xsl:value-of select="wt:data_name"/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="wt:data_name"/>
</xsl:otherwise>
</xsl:choose>
</div>
</xsl:element>
</xsl:template><xsl:template name="display_duration">
<xsl:param name="duration"/>
<xsl:if test="$duration!= ''">
<xsl:variable name="hours" select="substring-before($duration, ':')"/>
<xsl:variable name="minutes" select="substring-before(substring-after($duration, ':'), ':')"/>
<xsl:element name="div">
<xsl:attribute name="class">duration_display</xsl:attribute>
<xsl:text>The duration is </xsl:text>
<xsl:if test="floor($hours)>0">
<xsl:value-of select="floor($hours)"/>
<xsl:choose>
<xsl:when test="floor($hours)>1">
<xsl:text> hours</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text> hour</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:if test="floor($minutes)>0">
<xsl:if test="floor($hours)>0">
<xsl:text> and </xsl:text>
</xsl:if>
<xsl:value-of select="floor($minutes)"/>
<xsl:choose>
<xsl:when test="floor($minutes)>1">
<xsl:text> minutes</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text> minute</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:text> long.</xsl:text>
</xsl:element>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
The current resultant HTML:
<!DOCTYPE html PUBLIC "-//W3c//DTD HTML 4.01//EN" "http://www.w3c.org/tr/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>XML Test</title>
</head>
<body>
<h1>Category 1</h1>
<div class="alt_1">
<div class="duration_display">The duration is 1 hour and 5 minutes long.</div>
<div class="display"><a href="http://www.example.com/wee_test.html">Test (cat 1)</a></div>
</div>
<h1>Category 1</h1>
<div class="alt_0"><div class="display">Test (cat 1)</div></div>
<h1>Category 1</h1>
<div class="alt_1">
<div class="duration_display">The duration is 10 hours and 1 minute long.</div>
<div class="display">Test (cat 1)</div>
</div>
<h1>Category 2</h1>
<div class="alt_0">
<div class="duration_display">The duration is 21 hours and 35 minutes long.</div>
<div class="display">Test (cat 2)</div>
</div>
<h1>Category 2</h1>
<div class="alt_1"><div class="display"><a href="http://www.example.com/wee_test.html">Test (cat 2)</a></div></div>
<h1>Category 2</h1>
<div class="alt_0">
<div class="duration_display">The duration is 10 hours long.</div>
<div class="display">Test (cat 2)</div>
</div>
<h1>Category 2</h1>
<div class="alt_1"><div class="display"><a href="http://www.example.com/wee_test.html">Test (cat 2)</a></div></div>
<h1>Category 4</h1>
<div class="alt_0"><div class="display">Test (cat 4)</div></div>
<h1>Category 4</h1>
<div class="alt_1">
<div class="duration_display">The duration is 11 hours and 1 minute long.</div>
<div class="display">Test (cat 4)</div>
</div>
<h1>Category 4</h1>
<div class="alt_0"><div class="display">Test (cat 4)</div></div>
</body>
</html>
Now, the problem here is that I display a category header over every entry. I only want the header when the category changes.
Since I'm sorting on the category, I can be confident that I won't get any surprises. All the Category 1 elements will be together, all the Category 2 elements will follow, etc.
What I need to do is test the
wt:data_cat node of the current iteration with the same node from the previous iteration. If they are different, then I output the header. Otherwise, I skip the header. Simple? It certainly should be. It's a crying shame I haven't found a usable example of this yet.
I'm sure I'll get it very soon.
I should say that I have found many, many sites with similar discussions that can't seem to differentiate between node-sets and the main document. I need to winnow down a node-set, not re-parse the document.
Yuck.
I need to see if there is a way to make the sort stick, or just ensure that my data is already sorted when it comes into the transform. I can do that, as it is the result of a MySQL query.
Here's the code that doesn't work on random data:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:wt="http://www.example.com" exclude-result-prefixes="wt">
<xsl:output indent="yes" method="html" version="4.01" doctype-system="http://www.w3c.org/tr/html4/strict.dtd" doctype-public="-//W3c//DTD HTML 4.01//EN"/><xsl:template match="/">
<html>
<head>
<title>XML Test</title>
</head>
<body>
<xsl:variable name="cats" select="wt:weetest/wt:elementwrapper[wt:data_cat!='']"/>
<xsl:for-each select="$cats">
<xsl:sort select="wt:data_cat"/>
<xsl:sort select="@id"/>
<xsl:variable name="mypos_minus_one" select="position()-1"/>
<xsl:variable name="prev" select="$cats[$mypos_minus_one]/wt:data_cat"/>
<xsl:if test="wt:data_cat!= $prev">
<h1>Category <xsl:value-of select="wt:data_cat"/></h1>
</xsl:if>
<xsl:call-template name="render_one"/>
</xsl:for-each>
</body>
</html>
</xsl:template><xsl:template name="render_one">
<xsl:element name="div">
<xsl:attribute name="class">
<xsl:text>alt_</xsl:text>
<xsl:number value="position() mod 2" format="1"/>
</xsl:attribute>
<xsl:attribute name="id">
<xsl:text>id_</xsl:text>
<xsl:value-of select="@id"/>
</xsl:attribute>
<xsl:call-template name="display_duration">
<xsl:with-param name="duration" select="wt:data_duration"/>
</xsl:call-template>
<div class="display">
<xsl:choose>
<xsl:when test="wt:data_uri!= ''">
<xsl:element name="a">
<xsl:attribute name="href">
<xsl:value-of disable-output-escaping="yes" select="wt:data_uri"/>
</xsl:attribute>
<xsl:value-of select="wt:data_name"/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="wt:data_name"/>
</xsl:otherwise>
</xsl:choose>
</div>
</xsl:element>
</xsl:template><xsl:template name="display_duration">
<xsl:param name="duration"/>
<xsl:if test="$duration!= ''">
<xsl:variable name="hours" select="substring-before($duration, ':')"/>
<xsl:variable name="minutes" select="substring-before(substring-after($duration, ':'), ':')"/>
<xsl:element name="div">
<xsl:attribute name="class">duration_display</xsl:attribute>
<xsl:text>The duration is </xsl:text>
<xsl:if test="floor($hours)>0">
<xsl:value-of select="floor($hours)"/>
<xsl:choose>
<xsl:when test="floor($hours)>1">
<xsl:text> hours</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text> hour</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:if test="floor($minutes)>0">
<xsl:if test="floor($hours)>0">
<xsl:text> and </xsl:text>
</xsl:if>
<xsl:value-of select="floor($minutes)"/>
<xsl:choose>
<xsl:when test="floor($minutes)>1">
<xsl:text> minutes</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text> minute</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:text> long.</xsl:text>
</xsl:element>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Now, why did I do that whacky thing where I declared a "variable" called "mypos_minus_one"?
That's because "position()" is relative to the time at which it was called. XPath is all about iterating through the document/node-set, and has an internal pointer that changes for each node it tests. If I put the "position()" function into the previous item test, it won't work, because position() is constantly changing for every test. I wind up with the position of the last element. I need to "pin" the position I'm looking for beforehand.
The problem is that the one I'm looking for isn't actually the one I specified. That's because XPath is considering the node-set as an unsorted list.
The new XSLT file (wee_test.xsl):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:wt="http://www.example.com" exclude-result-prefixes="wt">
<xsl:output indent="yes" method="html" version="4.01" doctype-system="http://www.w3c.org/tr/html4/strict.dtd" doctype-public="-//W3c//DTD HTML 4.01//EN"/><xsl:template match="/">
<html>
<head>
<title>XML Test</title>
</head>
<body>
<xsl:variable name="cats" select="wt:weetest/wt:elementwrapper[wt:data_cat!='']"/>
<xsl:for-each select="$cats">
<xsl:variable name="mypos_minus_one" select="position()-1"/>
<xsl:variable name="prev" select="$cats[$mypos_minus_one]/wt:data_cat"/>
<xsl:if test="(position() = 1) or (wt:data_cat!= $prev)">
<h1>Category <xsl:value-of select="wt:data_cat"/></h1>
</xsl:if>
<xsl:call-template name="render_one"/>
</xsl:for-each>
</body>
</html>
</xsl:template><xsl:template name="render_one">
<xsl:element name="div">
<xsl:attribute name="class">
<xsl:text>alt_</xsl:text>
<xsl:number value="position() mod 2" format="1"/>
</xsl:attribute>
<xsl:attribute name="id">
<xsl:text>id_</xsl:text>
<xsl:value-of select="@id"/>
</xsl:attribute>
<xsl:call-template name="display_duration">
<xsl:with-param name="duration" select="wt:data_duration"/>
</xsl:call-template>
<div class="display">
<xsl:choose>
<xsl:when test="wt:data_uri!= ''">
<xsl:element name="a">
<xsl:attribute name="href">
<xsl:value-of disable-output-escaping="yes" select="wt:data_uri"/>
</xsl:attribute>
<xsl:value-of select="wt:data_name"/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="wt:data_name"/>
</xsl:otherwise>
</xsl:choose>
</div>
</xsl:element>
</xsl:template><xsl:template name="display_duration">
<xsl:param name="duration"/>
<xsl:if test="$duration!= ''">
<xsl:variable name="hours" select="substring-before($duration, ':')"/>
<xsl:variable name="minutes" select="substring-before(substring-after($duration, ':'), ':')"/>
<xsl:element name="div">
<xsl:attribute name="class">duration_display</xsl:attribute>
<xsl:text>The duration is </xsl:text>
<xsl:if test="floor($hours)>0">
<xsl:value-of select="floor($hours)"/>
<xsl:choose>
<xsl:when test="floor($hours)>1">
<xsl:text> hours</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text> hour</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:if test="floor($minutes)>0">
<xsl:if test="floor($hours)>0">
<xsl:text> and </xsl:text>
</xsl:if>
<xsl:value-of select="floor($minutes)"/>
<xsl:choose>
<xsl:when test="floor($minutes)>1">
<xsl:text> minutes</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text> minute</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
<xsl:text> long.</xsl:text>
</xsl:element>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
The new, pre-sorted XML file (wee_test.xml):
<?xml version="1.0" encoding="ISO-8859-1"?>
<weetest xmlns="http://www.example.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.example.com file:/Users/cmarshall/Desktop/XMLTest/wee_test/wee_test.xsd">
<elementwrapper id="1">
<data_cat>1</data_cat>
<data_duration>01:05:00</data_duration>
<data_name>Test (cat 1)</data_name>

</elementwrapper>
<elementwrapper id="2">
<data_cat>1</data_cat>
<data_name>Test (cat 1)</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="8">
<data_cat>1</data_cat>
<data_duration>10:01:00</data_duration>
<data_name>Test (cat 1)</data_name>
</elementwrapper>
<elementwrapper id="3">
<data_cat>2</data_cat>
<data_duration>21:35:00</data_duration>
<data_name>Test (cat 2)</data_name>
</elementwrapper>
<elementwrapper id="4">
<data_cat>2</data_cat>
<data_name>Test (cat 2)</data_name>

</elementwrapper>
<elementwrapper id="7">
<data_cat>2</data_cat>
<data_duration>10:00:01</data_duration>
<data_name>Test (cat 2)</data_name>
</elementwrapper>
<elementwrapper id="9">
<data_cat>2</data_cat>
<data_name>Test (cat 2)</data_name>

</elementwrapper>
<elementwrapper id="0">
<data_cat>4</data_cat>
<data_name>Test (cat 4)</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="5">
<data_cat>4</data_cat>
<data_name>Test (cat 4)</data_name>
<data_uri/>
</elementwrapper>
<elementwrapper id="6">
<data_cat>4</data_cat>
<data_duration>11:01:20</data_duration>
<data_name>Test (cat 4)</data_name>
</elementwrapper>
</weetest>
The schema hasn't changed.
The resultant HTML:
<!DOCTYPE html PUBLIC "-//W3c//DTD HTML 4.01//EN" "http://www.w3c.org/tr/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>XML Test</title>
</head>
<body>
<h1>Category 1</h1>
<div class="alt_1" id="id_1">
<div class="duration_display">The duration is 1 hour and 5 minutes long.</div>
<div class="display"><a href="http://www.example.com/wee_test.html">Test (cat 1)</a></div>
</div>
<div class="alt_0" id="id_2"><div class="display">Test (cat 1)</div></div>
<div class="alt_1" id="id_8">
<div class="duration_display">The duration is 10 hours and 1 minute long.</div>
<div class="display">Test (cat 1)</div>
</div>
<h1>Category 2</h1>
<div class="alt_0" id="id_3">
<div class="duration_display">The duration is 21 hours and 35 minutes long.</div>
<div class="display">Test (cat 2)</div>
</div>
<div class="alt_1" id="id_4"><div class="display"><a href="http://www.example.com/wee_test.html">Test (cat 2)</a></div></div>
<div class="alt_0" id="id_7">
<div class="duration_display">The duration is 10 hours long.</div>
<div class="display">Test (cat 2)</div>
</div>
<div class="alt_1" id="id_9"><div class="display"><a href="http://www.example.com/wee_test.html">Test (cat 2)</a></div></div>
<h1>Category 4</h1>
<div class="alt_0" id="id_0"><div class="display">Test (cat 4)</div></div>
<div class="alt_1" id="id_5"><div class="display">Test (cat 4)</div></div>
<div class="alt_0" id="id_6">
<div class="duration_display">The duration is 11 hours and 1 minute long.</div>
<div class="display">Test (cat 4)</div>
</div>
</body>
</html>