Reputation: 2484
I have a long list in XML and I have to sort it using a derived property that is computed selecting values of other elements stored in the same XML file. It is possible to do something like this (preferably using XSLT 1.0)?
I post an example code.
This is my XML:
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="example.xslt"?>
<example>
<references>
<value x="0" y="0" res="0" />
<value x="1" y="1" res="5" />
<value x="1" y="2" res="3" />
<value x="2" y="1" res="1" />
</references>
<list>
<item x="0" y="0">...</item>
<item x="1" y="2">...</item>
<item x="2" y="1">...</item>
</list>
</example>
And this is the XSLT:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:output method="html" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html>
<head>
</head>
<body>
<table>
<xsl:for-each select="//list/item">
<xsl:variable name="x">
<xsl:value-of select="@x" />
</xsl:variable>
<xsl:variable name="y">
<xsl:value-of select="@y" />
</xsl:variable>
<xsl:variable name="order">
<xsl:for-each select="//references/value">
<xsl:if test="@x=$x and @y=$y">
<xsl:value-of select="@res" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
<!-- I want to do something like this: -->
<!--<xsl:sort select="$order" />-->
<tr>
<td><xsl:value-of select="text()" /></td>
<td><xsl:value-of select="$order" /></td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
Thanks for your help!
Upvotes: 0
Views: 586
Reputation: 163322
In XSLT 1.0 the sort key has to be a single XPath expression, but as others have shown you can do quite a lot in a single XPath expression. In XSLT 2.0 things become easier because your XPath expression can call a stylesheet function to do the computation, and you can also use instructions within the body of the xsl:sort. You still can't construct variables before the xsl:sort, however - we looked at that and it gets very complicated.
A couple of points about your code.
You don't need to write this:
<xsl:variable name="x">
<xsl:value-of select="@x" />
</xsl:variable>
when you can write this (which is also likely to be much more efficient):
<xsl:variable name="x" select="@x"/>
And you don't need to write this:
<xsl:for-each select="//references/value">
<xsl:if test="@x=$x and @y=$y">
<xsl:value-of select="@res" />
</xsl:if>
</xsl:for-each>
when you can write this:
<xsl:value-of select="//references/value[@x=$x and @y=$y]/@res"/>
So generally, you're not using the full power of the language.
Upvotes: 3
Reputation: 43168
It's not as complex as you think. The thing you need is current()
.
You can find the relevant value
in a single XPath expression by matching the "remote" node against the current node, with no need for variables to capture them. As such, you can do the sort straightforwardly in XSLT 1.0:
<xsl:sort select="//references/value
[@x = current()/@x]
[@y = current()/@y]
/@res"
data-type="number" />
The following transform, applied to your input,
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
<xsl:output method="html" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html>
<head>
</head>
<body>
<table>
<xsl:for-each select="//list/item">
<xsl:sort select="//references/value
[@x = current()/@x]
[@y = current()/@y]
/@res"
data-type="number" />
<xsl:variable name="order"
select="//references/value
[@x = current()/@x]
[@y = current()/@y]
/@res"/>
<tr>
<td><xsl:value-of select="text()" /></td>
<td><xsl:value-of select="$order" /></td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
Gives the following output:
<html xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:fn="http://www.w3.org/2005/xpath-functions">
<head>
<META http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body>
<table>
<tr>
<td>...</td>
<td>0</td>
</tr>
<tr>
<td>...</td>
<td>1</td>
</tr>
<tr>
<td>...</td>
<td>3</td>
</tr>
</table>
</body>
</html>
However, if you want to get the value of @res
within the template, note that you have to repeat the expression. Alternatively, you could use a key
:
<xsl:stylesheet>
...
<xsl:key name="xy" match="value" use="concat(@x, ':', @y)" />
...
<xsl:for-each select="//list/item">
<xsl:sort select="key('xy', concat(@x, ':', @y))/@res" data-type="number" />
<xsl:variable name="order"
select="key('xy', concat(@x, ':', @y))/@res"/>
...
Which gives the same output with less repetition.
Upvotes: 1
Reputation: 167516
Define a key <xsl:key name="k1" match="references/value" use="concat(@x, '|', @y)"/>
, then simply write the <xsl:sort select="key('k1', concat(@x, '|', @y))/@res"/>
as the first child of the for-each.
Upvotes: 1