O'Reilly logo

XSLT Cookbook by Sal Mangano

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Determining if Two Nodes Are the Same

Problem

You need to determine if two separate references to a node are the same node.

Solution

If the compared nodes are element nodes with a unique attribute of type ID, then comparison is most conveniently made by comparing these attributes. If this is not the case, then use generate-id, as in:

<xsl:if test="generate-id($node1) = generate-id($node2)">

Here we assume $node1 and $node2 are node sets containing a single node.

An interesting generalization of this test checks if all the nodes in one node-set are the same as all the nodes in another. For this task, generate-id is not useful because it only generates an ID for the first node in document order. Instead, you need to take advantage the XPath union operator’s (|) ability to determine node equality.

<xsl:if test="count($ns1|$ns2) = count($ns1) and 
               count($ns1) = count($ns2)">

In other words, if two node sets have the same number of nodes, and the number of nodes resulting from the union of both node sets is the same as the number of nodes in one of those two original node sets, then they must be the same sets. If they are not, then the union must contain at least one more node than either individual set.

If you only care if $ns2 is either equal to or a subset of $ns1, then you can simply write:

<xsl:if test="count($ns1|$ns2) = count($ns1)">

On the other hand, if you want to test that $ns2 is a proper subset of $ns1, then you need to write:[8]

<xsl:if test="count($ns1|$ns2) = count($ns1) and count($ns1) > count(ns2)">

From these examples, you should also conclude that the alternative to using generate-id( ) to test equality in the single node instance is to write:

<xsl:if test="count($ns1|$ns2) = 1">

Discussion

You can apply these techniques to make sophisticated queries against a document. For example, consider the stylesheet shown in Example 4-14 that analyzes SalesBySalesPerson.xml to find products sold by common sets of salespeople. Its output is listed in Example 4-15.

Example 4-14. products-sold-in-common.xslt

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   
<xsl:output method="text"/>
   
<!-- Extract the unique set of products -->     
<xsl:template match="/">
     <xsl:call-template name="process-products">
          <xsl:with-param name="products"
          select="/*/salesperson/product[not(@sku=preceding::product/@sku)]"/>
     </xsl:call-template> 
</xsl:template>
     
<!-- Process all pairs of products -->
<xsl:template name="process-products">
     <xsl:param name="products"/>
     <xsl:for-each select="$products">
       <xsl:variable name="product1" select="."/>
       <xsl:for-each select="$products">
         <xsl:variable name="product2" select="."/>
          <-- Don't analyze the product against itself -->
          <xsl:if test="generate-id($product1) != generate-id($product2)">
            <xsl:call-template name="show-products-sold-in-common">
              <xsl:with-param name="product1" select="$product1"/>
              <xsl:with-param name="product2" select="$product2"/>
            </xsl:call-template>
          </xsl:if>
       </xsl:for-each>
     </xsl:for-each>
</xsl:template>
   
<!-- Determine if two products have salespeople in common. -->
<xsl:template name="show-products-sold-in-common">
     <xsl:param name="product1"/>
     <xsl:param name="product2"/>
     <xsl:variable name="who-sold-p1"
          select="//salesperson[product/@sku = $product1/@sku]"/>
     <xsl:variable name="who-sold-p2"
          select="//salesperson[product/@sku = $product2/@sku]"/>
          
     <!-- If those who sold product2 is a subset of those who sold product1
         then they have these products have common salespeople -->
     <xsl:if test="count($who-sold-p1|$who-sold-p2) = count($who-sold-p1)">
       <xsl:text>All the salespeople who sold product </xsl:text>
       <xsl:value-of select="$product2/@sku"/>
       <xsl:text> also sold product </xsl:text>
       <xsl:value-of select="$product1/@sku"/>
       <!-- If the counts of both sets are also equal then the two sets are 
          actually the same. -->
       <xsl:if test="count($who-sold-p1) = count($who-sold-p2)">
         <xsl:text> and vice versa</xsl:text>
       </xsl:if>
       <xsl:text>.&#xa;</xsl:text>
     </xsl:if>
</xsl:template>
     
</xsl:stylesheet>

Example 4-15. Output

All the salespeople who sold product 20000 also sold product 10000 and vice versa.
All the salespeople who sold product 25000 also sold product 10000.
All the salespeople who sold product 30000 also sold product 10000.
All the salespeople who sold product 70000 also sold product 10000.
All the salespeople who sold product 10000 also sold product 20000 and vice versa.
All the salespeople who sold product 25000 also sold product 20000.
All the salespeople who sold product 30000 also sold product 20000.
All the salespeople who sold product 70000 also sold product 20000.
All the salespeople who sold product 70000 also sold product 25000.
All the salespeople who sold product 70000 also sold product 30000.

To stay focused on node-identity testing, this example did not present the most efficient solution to this task. The following solution is preferable in this specific case:

<xsl:template name="process-products">
  <xsl:param name="products"/>
  <xsl:for-each select="$products">
     <xsl:variable name="product1" select="."/>
     <xsl:variable name="pos" select="position(  )"/>
     <xsl:for-each select="$products[position(  ) > $pos]">
      <xsl:variable name="product2" select="."/>
      <xsl:call-template name="show-products-sold-in-common">
           <xsl:with-param name="product1" select="$product1"/>
           <xsl:with-param name="product2" select="$product2"/>
      </xsl:call-template>
     </xsl:for-each>
  </xsl:for-each>
</xsl:template>

Notice how the solution avoids generate-id( ) by relying on the uniqueness of position within a single node set. However, if there were two independent node sets rather than one, you wouldn’t necessarily rely on position as an indicator of identity.

In addition, tests like:

<xsl:variable name="who-sold-p1"     
             select="//salesperson[product/@sku = $product1/@sku]"/>

might be done more efficiently with a key, as was suggested in Recipe 4.1:

<xsl:key name="sp_key" match="salesperson" use="product/@sku"/>
   
<xsl:variable name="who-sold-p1"  select="key('sp_key',$product1/@sku)"/>

See Also

Testing node equality has important applications in grouping problems. See Chapter 5 for examples of grouping.



[8] Recall that a proper subset does not include the entire set as a subset.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required