OK, I want to apply a XSL style sheet that counts the previous unique "ROLE" nodes and spits out the following output format of @name the number of unique ROLE nodes prior to the current nodes. I've wasted several hours on what should be an easy thing to implement. I have tried to implement this in several ways including the Muenchian Method, if/with variables (Can't increment a variable), applying templates to templates etc to no avail.
I have the following XML:
<ROLEACTIONINFO>
<ROLE name="TESTER" />
<ROLE name="PARENT1"/>
<ROLE name="PARENT1"/>
<ROLE name="PARENT1"/>
<ROLE name="PARENT2"/>
<ROLE name="PARENT2"/>
<ROLE name="PARENT3"/>
<ROLE name="PARENT4"/>
<ROLE name="TESTROLE"/>
</ROLEACTIONINFO>
OUTPUT EXAMPLE:
TESTER 1
PARENT1 2
PARENT1 2
PARENT1 2
PARENT2 3
PARENT2 3
PARENT3 4
PARENT4 5
TESTROLE 6
Getting the count of the unique preceeding nodes is my problem. Any help would be appreciated
This can be solved pretty easily using XPath. Here's the expression you're looking for: count((.|preceding-sibling::ROLE)[not(@name = preceding-sibling::ROLE/@name)])
This can be broken down to make it more readable, as I've done in the following XSLT 1.0 stylesheet:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<!-- don't copy whitespace -->
<xsl:template match="text()"/>
<xsl:template match="ROLE">
<xsl:variable name="roles-so-far" select=". | preceding-sibling::ROLE"/>
<!-- Only select the first instance of each ROLE name -->
<xsl:variable name="roles-so-far-unique"
select="$roles-so-far[not(@name = preceding-sibling::ROLE/@name)]"/>
<xsl:apply-templates select="@name"/>
<xsl:text> </xsl:text>
<xsl:value-of select="count($roles-so-far-unique)"/>
<xsl:text>
</xsl:text> <!-- linefeed -->
</xsl:template>
</xsl:stylesheet>
Here's an alternative implementation, using the Muenchian method. First, declare a key:
<xsl:key name="roles" match="ROLE" use="@name"/>
Then, replace the definition of $roles-so-far-unique with something like this:
<!-- Among all the ROLEs having one of the names so far,
select only the first one for each name -->
<xsl:variable name="roles-so-far-unique"
select="../ROLE[@name = $roles-so-far/@name]
[generate-id(.) = generate-id(key('roles',@name)[1])]"/>
This code, of course, is more complicated. Unless you have a large data set requiring you to speed up processing using the Muenchian method (even then I would test to make sure it buys you anything), you might as well stick with the simpler version above.
Finally, in XSLT 2.0, it's much easier. Simple replace the $roles-so-far-unique definition with the following:
<!-- Return a list of distinct string values, with duplicates removed -->
<xsl:variable name="roles-so-far-unique"
select="distinct-values($roles-so-far/@name)"/>
I hope this has helped you identify where you went wrong in the various attempts that you mentioned.
This is easily solved with an <xsl:key>
:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output method="text" />
<xsl:key name="kRole" match="ROLE" use="@name" />
<xsl:template match="ROLE">
<xsl:value-of select="concat(@name, ' ')" />
<xsl:value-of select="count(
(. | preceding-sibling::ROLE)[
count(. | key('kRole', @name)[1]) = 1
])" />
</xsl:template>
</xsl:stylesheet>
Output is as desired:
TESTER 1
PARENT1 2
PARENT1 2
PARENT1 2
PARENT2 3
PARENT2 3
PARENT3 4
PARENT4 5
TESTROLE 6
Explanation of the XPath expression in the <xsl:value-of>
:
count( # count the nodes:
(. | preceding-sibling::ROLE) # union of this node and its predecessors
[ # where...
count( # the count of the union of...
. | # this node and
key('kRole', @name)[1] # the first node with the same @name
) = 1 # is 1
]
)
This is the Muenchian method. Based on the fact that a node set cannot contain the same node twice, a union of two nodes has a node count of 1 if they are the same node. This way we are selecting the unique nodes from (. | preceding-sibling::ROLE)
only.
If there are more than one <ROLEACTIONINFO>
elements in your document, there is a parent check missing. This is also easily achieved:
<xsl:template match="ROLE">
<xsl:variable name="parentId" select="generate-id(..)" />
<xsl:value-of select="count(
(. | preceding-sibling::ROLE)[
count(. | key('kRole', @name)[generate-id(..) = $parentId][1]) = 1
])" />
</xsl:template>
Note that [generate-id(..) = $parentId][1]
!= [1][generate-id(..) = $parentId]
.
Order is important when chaining predicates. The former checks for parent node equality first and then takes the first unique node from the reduced set. This is what we want.
The latter takes the first node from the set (all ROLE nodes with a given name throughout the document), takes the first one and then keeps or discards it based on parent equality. This is wrong.
Recursion usually works pretty well with problems like this.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" media-type="text/plain" />
<xsl:template name="count-previous-but-not-with-my-name">
<xsl:param name="nodes" />
<xsl:param name="count" select="0" />
<xsl:choose>
<xsl:when test="count($nodes) = 0">
<xsl:value-of select="$count" />
</xsl:when>
<xsl:otherwise>
<xsl:variable name="last-name" select="$nodes[last()]/@name" />
<xsl:variable name="nodes-before-me-without-my-name" select="$nodes[position() < last() and @name != $last-name]" />
<xsl:call-template name="count-previous-but-not-with-my-name">
<xsl:with-param name="nodes" select="$nodes-before-me-without-my-name" />
<xsl:with-param name="count" select="$count + 1" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="/">
<xsl:for-each select="//ROLEACTIONINFO/ROLE">
<xsl:variable name="role" select="current()" />
<xsl:variable name="my-pos" select="position()" />
<xsl:value-of select="current()/@name" /><xsl:text> </xsl:text>
<xsl:call-template name="count-previous-but-not-with-my-name">
<xsl:with-param name="nodes" select="$role/../ROLE[position() <= $my-pos]" />
</xsl:call-template>
<xsl:text> </xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>