Reputation: 3507
How to sort <foo name="..."/>
nodes from $foo using their corresponding node order from $bar?
Partial solution (Mark Veenstra)
<xsl:for-each select="$list-resources">
<xsl:apply-templates select="$building-resources[@name = current()/@name]">
<xsl:with-param name="param" select="$some-value"/>
</xsl:apply-templates>
</xsl:for-each>
New problem (see edit at the end of the question):
How to retrieve the position in the whole sorted node set of each $building-resources node?
The <xsl:sort/>
has an attribute, @data-type
, to specify the type of data in the @select
node. Default is "text". Since I compare numbers, I must set @data-type="number"
. Else, the text comparison fails when it comes to compare 9 and 10.
<xsl:sort select="count($bar[@name = current()/@name]/preceding-sibling::*)" data-type="number"/>
In <xsl:sort/>
, current()
refers to the currently sorted node (not to the <xsl:template/>
current node)
Thanks to michael.hor257k
Code
<!-- Resource "instances" to order, using $list-resources -->
<xsl:variable name="building-resources"
select="document('building.xml')/building/resource"/>
<result>
<!-- Apply templates to the resource instances -->
<xsl:apply-templates select="$building-resources">
<!-- Sort using the nodes order from the resources list -->
<!-- Don't forget the @data-type: we're comparing NUMBERS not TEXTS -->
<xsl:sort
select="count($list-resources[@name = current()/@name]/preceding-sibling::*)"
data-type="number"/>
</xsl:apply-templates>
</result>
</xsl:template>
<!-- Template to apply for each resource of $building-resources -->
<xsl:template match="ressource">
<!-- position() refers to the node position in $building-resources
because we used $building-resources as @select value
of the apply-templates -->
<output position="{position()}">
<xsl:value-of select="."/>
</output>
</xsl:template>
Inputs
list-resources.xml
<resources>
<resource name="wood"/>
<resource name="stone"/>
<resource name="gold"/>
</resources>
building-resources.xml
<building>
<resource name="stone"/>
<resource name="gold"/>
<resource name="stone"/>
<resource name="wood"/>
<resource name="wood"/>
</building>
Output
<result>
<output position="1">wood</output>
<output position="2">wood</output>
<output position="3">stone</output>
<output position="4">stone</output>
<output position="5">gold</output>
</result>
Sorting
I have two XML documents:
A list of resources (list-resources.xml
, like a "class" list):
<resources>
<resource name="wood"/>
<resource name="stone"/>
<resource name="gold"/>
</resources>
And resources inside a building (buildings.xml
, like "objects" list):
<building>
<resource name="stone"/>
<resource name="gold"/>
<resource name="stone"/>
<resource name="wood"/>
<resource name="wood"/>
</building>
I want to order the <resource/>
nodes from <building/>
so it matches the <resource/>
order from <resources/>
. This is the desired output:
<result>
<output position="1">wood</output>
<output position="2">wood</output>
<output position="3">stone</output>
<output position="4">stone</output>
<output position="5">gold</output>
</result>
To do so, I have two node-sets in a XSL:
<xsl:variable name="building-resources" select="building/resource"/>
<xsl:variable name="list-resources" select="resources/resource"/>
And I use an XSL apply-templates to treat nodes from $building-resources
:
<xsl:apply-templates select="$building-resources">
<xsl:with-param name="some-param" select="$variable-from-somewhere"/>
</xsl:apply-templates>
Let's say the applied templates is ok. My currently not-sorted result is:
<result>
<output position="1">stone</output>
<output position="2">gold</output>
<output position="3">stone</output>
<output position="4">wood</output>
<output position="5">wood</output>
</result>
Now, I added a <xsl:sort/>
element to sort my nodes from $building-resources
, but I don't know what to put inside its @select
...
<xsl:apply-templates select="$building-resources">
<xsl:sort select="what-to-put-here"/>
<xsl:with-param name="some-param" select="$variable-from-somewhere"/>
</xsl:apply-templates>
I tried the following XPath expression:
count($list-ressources[@name = current()/@name]/preceding-sibling::*)
But inside this XPath,
But this doesn't work, the result is not correctly sorted.
I would mean "the node the current()
refers to the node treated by the <xsl:template/>
where <xsl:apply-templates/>
is.
So, instead of current()
, <xsl:sort/>
process is currently treating" (edit: this is actually exactly what current() does!) inside my XPath brackets $list-resources[]
. How do I do so?
Get the sorted position
Since I simplified the code, I forgot something... Mark Veenstra's solution raised up a problem.
In the applied template (the template that generates the <output/>
node), I use position()
to get the position of the node the template is applied on:
<xsl:template match="resource">
<xsl:variable name="position" select="position()"/>
<output position="{$position}">
<xsl:value-of select="@name"/>
</output>
</xsl:template>
This works well as long as I do only "one" apply-templates (hence the position()
inside the this applied template holds the position of the node in the sorted list). But if I apply Mark's solution (note that current()
inside the <xsl:sort/>
now refers to the node from the <xsl:for-each/>
aka from $liste-resources):
<xsl:for-each select="$list-resources">
<xsl:apply-templates select="$building-resources[@name = current()/@name]">
<xsl:with-param name="param" select="$some-value"/>
</xsl:apply-templates>
</xsl:for-each>
Then the position()
inside the applied template refers to the position of the node in the partial node set ($building-resources[@name = current()/@name]).
How to fix that?
Upvotes: 2
Views: 2602
Reputation: 117003
Regarding the sorting problem, I would suggest:
<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:template match="/">
<building>
<xsl:apply-templates select="building/resource">
<xsl:sort select="count(document('list-resources.xml')/resources/resource[@name = current()/@name]/preceding-sibling::resource)" data-type="number" order="ascending"/>
</xsl:apply-templates>
</building>
</xsl:template>
<xsl:template match="resource">
<resource position="{position()}">
<xsl:value-of select="@name"/>
</resource>
</xsl:template>
</xsl:stylesheet>
Applied to your input example, this returns:
<?xml version="1.0" encoding="UTF-8"?>
<building>
<resource position="1">wood</resource>
<resource position="2">wood</resource>
<resource position="3">stone</resource>
<resource position="4">stone</resource>
<resource position="5">gold</resource>
</building>
I am not sure what the other problem is.
Upvotes: 2
Reputation: 338248
I'd use an XSL key and <xsl:sort>
, like this:
<xsl:key name="kResource" match="resources/resource" use="@name" />
<!-- regular identity template -->
<xsl:template match="node() | @*">
<xsl:copy>
<xsl:apply-templates select="node() | @*" />
</xsl:copy>
</xsl:template>
<!-- specialized identity template, adds a position attribute -->
<xsl:template match="*" mode="with-position">
<xsl:copy>
<xsl:attribute name="position"><xsl:value-of select="position()" /></xsl:attribute>
<xsl:apply-templates select="node() | @*" />
</xsl:copy>
</xsl:template>
<xsl:template match="building">
<result>
<xsl:apply-templates select="*" mode="with-position">
<xsl:sort select="count(key('kResource', @name)/preceding-sibling::*)" data-type="number" />
</xsl:apply-templates>
</result>
</xsl:template>
Relevant output:
<result>
<resource position="1" name="wood" />
<resource position="2" name="wood" />
<resource position="3" name="stone" />
<resource position="4" name="stone" />
<resource position="5" name="gold" />
</result>
Note that even though I've added a position
attribute, I think that's a rather unnecessary thing to have. XML elements have a natural position in the document, no need to be over-specific.
Upvotes: 0
Reputation: 4739
I think you should try to do something like the following. (untested)
Change:
<xsl:apply-templates select="$building-resources">
<xsl:sort select="what-to-put-here"/>
<xsl:with-param name="some-param" select="$variable-from-somewhere"/>
</xsl:apply-templates>
To:
<xsl:for-each select="$list-resources/@name">
<xsl:variable name="curName" select="." />
<xsl:apply-templates select="$building-resources[@name = $curName]">
<xsl:with-param name="some-param" select="$variable-from-somewhere"/>
</xsl:apply-templates>
</xsl:for-each>
So before applying the templates, make sure you apply in the order of the other nodeset. That's why I added an extra for-each
around it.
Upvotes: 2