Reputation: 45
I am trying to transform an XML document into a list where the values are based on the attribute of preceding siblings.
Sample XML:
<myRoot>
<Person>Craig</Person>
<Person rank="10">Woody</Person>
<Person>Brian</Person>
<Person>Michael</Person>
<Person rank="20">Emily</Person>
<Person>Chris</Person>
</myRoot>
What I want:
<myNewRoot>
<Index>1: Craig</Index>
<Index>10: Woody</Index>
<Index>11: Brian</Index>
<Index>12: Michael</Index>
<Index>20: Emily</Index>
<Index>21: Chris</Index>
</myNewRoot>
I'm stuck and unable to determine the distance between the last preceeding sibiling with the @rank attribute and the current node.
Here is my current stylesheet
<xsl:template match="Person">
<xsl:element name="Index">
<xsl:choose>
<xsl:when test="./@rank">
<xsl:value-of select="./@rank"/>
</xsl:when>
<xsl:when test="preceding-sibling::Person[@rank]">
<xsl:value-of select="count(.|preceding-sibling::*[. > current()/preceding-sibling::Person[@rank][1]]) + preceding-sibling::Person[@rank][1]/@rank"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="position()"/>
</xsl:otherwise>
</xsl:choose>
<xsl:text>: </xsl:text>
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>
I just can't get the count() functionality to work and keep ending up with
<myNewRoot>
<Index>1: Craig</Index>
<Index>10: Woody</Index>
<Index>11: Brian</Index>
<Index>11: Michael</Index>
<Index>20: Emily</Index>
<Index>21: Chris</Index>
</myNewRoot>
Upvotes: 2
Views: 4696
Reputation: 1920
A solution would be using a recursive template to obtain the current value based on the previous value printed, which is passed as a param to the template.
<xsl:output method="xml" indent="yes" />
<xsl:template match="myRoot">
<myNewRoot>
<xsl:call-template name="make-index" />
</myNewRoot>
</xsl:template>
<xsl:template name="make-index">
<xsl:param name="element" select="Person" />
<xsl:param name="count" select="'0'" />
<!-- Continue if there is some element left -->
<xsl:if test="$element">
<!-- Obtain number to be printed next -->
<xsl:variable name="next-rank">
<xsl:choose>
<!-- If the rank attribute is present, output its value -->
<xsl:when test="$element[1]/@rank">
<xsl:value-of select="$element[1]/@rank" />
</xsl:when>
<!-- If the rank attribute is not present, increase the previous
printed value by one -->
<xsl:otherwise>
<xsl:value-of select="$count + 1" />
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<!-- Output the index along with the value of the current index -->
<Index>
<xsl:value-of select="concat($next-rank, ': ', $element[1])" />
</Index>
<!-- Recurse until we do not have any element left -->
<xsl:call-template name="make-index">
<xsl:with-param name="element" select="$element[position() > 1]" />
<xsl:with-param name="count" select="$next-rank" />
</xsl:call-template>
</xsl:if>
</xsl:template>
UPDATE. The following solution do not rely on recursion, probably is not as efficient as the previous one (in this one there are more complex XPath operations) but is shorter and relies in grouping siblings which is a different approach compared with the previous one.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<xsl:template match="myRoot">
<myNewRoot>
<!-- Print all the elements before the first element with a rank
attribute defined -->
<xsl:apply-templates select="Person[(preceding-sibling::Person[@rank])][not(@rank)]" mode="print" />
<!-- Match all the elements with a rank attribute defined -->
<xsl:apply-templates select="Person[@rank]" />
</myNewRoot>
</xsl:template>
<!-- Match the set of Person elements with @rank defined -->
<xsl:template match="Person[@rank]">
<!-- Obtain id of current node before losing the context -->
<xsl:variable name="id" select="generate-id()" />
<!-- Match the current node along all the following siblings without @rank such
as their nearest preceding-sibling with @rank defined is the current
element, i.e all the elements between the current element and the next
element with @rank defined -->
<xsl:apply-templates select=".|following-sibling::Person[not(@rank)][generate-id(preceding-sibling::Person[@rank][1]) = $id]" mode="print">
<xsl:with-param name="rank" select="@rank" />
</xsl:apply-templates>
</xsl:template>
<!-- Print the information from a Person node, using rank to determine
the position -->
<xsl:template match="Person" mode="print">
<xsl:param name="rank" select="'1'" />
<Index>
<xsl:value-of select="concat($rank + position() - 1, ': ', .)" />
</Index>
</xsl:template>
NOTE: I am assuming for both solutions that you are using XSLT 1.0. In case you are using XSLT 2.0 the solution would be easier than the previous ones.
Upvotes: 2