Reputation: 27579
I'm using the Muenchian method to group nodes in an XML document.
As part of my output, I'd like to select all of the nodes to which I have not assigned a key.
I've tried
<xsl:apply-templates select="*[key('kcWWPN','')]"/>
but that doesn't appear to be working correctly, in that it is selecting no nodes.
Any suggestions on the right way to do this?
Upvotes: 3
Views: 2210
Reputation: 243579
As part of my output, I'd like to select all of the nodes to which I have not assigned a key.
Good question, +1.
Here are two different, but simple solutions:
For simplicity I will assume that we are keying only elements, but the two solutions below can naturally be extended to cover other types of nodes.
I. Simply, define a counter-key:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kDByClass" match="d" use="@class"/>
<xsl:key name="kCounterKey" match="*[not(self::d)]" use="."/>
<xsl:template match="*[key('kCounterKey', .)]">
Counter Keyed: <xsl:value-of select="name()"/>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
when this transformation is applied on the following XML document (borrowed from @lwburk):
<root>
<d class="test">1</d>
<d class="test">2</d>
<d class="something">1</d>
<q>3</q>
</root>
the wanted, correct result is produced:
Counter Keyed: root
Counter Keyed: q
II. Use the set difference of all elements and all keyed elements (the latter is found using Muenchian grouping):
This solution is simpler than the first, because one doesn't need to compose a counter-key:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kDByClass" match="d" use="@class"/>
<xsl:key name="kCounterKey" match="*[not(self::d)]" use="."/>
<xsl:variable name="vKeyedValues" select=
"//*[generate-id()
= generate-id(key('kDByClass', @class)[1])
]
/@class
"/>
<xsl:variable name="vKeyedElements" select=
"key('kDByClass', $vKeyedValues)"/>
<xsl:variable name="vNonKeyedElements" select=
"//*[not(count(.|$vKeyedElements) = count($vKeyedElements))]
"/>
<xsl:template match="/">
<xsl:for-each select="$vNonKeyedElements">
Not Keyed: <xsl:value-of select="name()"/>
</xsl:for-each>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
when this transformation is applied on the same XML document (above), again the same, correct result is produced:
Not Keyed: root
Not Keyed: q
Note: The last solution may not work when the key is not a node, but result of a function, such as substring-before()
. In this case we simply modify the original key, so thet its use
attribute is simply: use="."
and use this solution with the modified original key. It can be prooven that this procedure produces the correct, wanted set of elements.
Upvotes: 5
Reputation: 60424
You simply need to re-create the key that would have been used for the nodes you want to select and attempt to retrieve the values for that key. If the set returned for that key does not contain the current node, then it should be selected.
For example, suppose the keys were created using the concatenation of each element's class
attribute value and its string value. We could select all elements in the document that do not have an assigned key using the following:
<xsl:apply-templates
select="//*[not(count(.|key('byClass', concat(@class, '|', .)))=
count(key('byClass', concat(@class, '|', .))))]"/>
Here's a complete demo:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="byClass" match="d" use="concat(@class, '|', .)"/>
<xsl:template match="/">
<xsl:apply-templates
select="//*[not(count(.|key('byClass', concat(@class, '|', .)))=
count(key('byClass', concat(@class, '|', .))))]"
mode="test"/>
</xsl:template>
<xsl:template match="*" mode="test">
<xsl:value-of
select="concat('Node ', local-name(),
' not assigned a key', '
')"/>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
Applied to this input:
<root>
<d class="test">1</d>
<d class="test">2</d>
<d class="something">1</d>
<q class="test">1</q>
</root>
It produces:
Node root not assigned a key
Node q not assigned a key
Note that the key produced by q
is an actual key that maps to a group of nodes in the document, but this element is not in the set returned by key('byClass', 'test|1')
, so we say that this node has not been assigned a key.
Note also that the empty string (''
) is a perfectly valid key, which is why this doesn't do what you were hoping it would:
<xsl:apply-templates select="*[key('kcWWPN', '')]"/>
Upvotes: 2