I would like to be able to compare two nodes based on the values of their child nodes. Testing node equality with the =
operator just compares the string values of the respective nodes. I would like to compare them based on values in their child nodes.
To be a bit more specific, I would like <a>
and <b>
(below) to be equal, because the values of @id
are the same for <c>
elements that have matching @type
attributes also have matching @id
attributes.
<a>
<c type="type-one" id="5675"/>
<c type="type-two" id="3423"/>
<c type="type-three" id="9088"/>
</a>
<b>
<c type="type-one" id="5675"/>
<c type="type-two" id="3423"/>
</b>
But these would be different:
<a>
<c type="type-one" id="5675"/>
</a>
<b>
<c type="type-one" id="2342"/>
</b>
The only solution I can begin to see involves a laborious comparison with a for-each
statement, which I would like to avoid if possible.
If possible I would like to stick with XSLT 1.0. I am using xsltproc.
First of all, the relation called "equals" cannot have that name.
"Equals" means that the relation is an equivalence relation. By definition any equivalence relation ~
must be:
Reflexive: x ~ x
.
Symmetric: if x ~ y
then y ~ x
Transitive: if x ~ y
and y ~ z
then x ~ z
.
Here is an example, showing that the proposed "equals" relation isn't transitive:
x
is:
<a>
<c type="type-one" id="5675"/>
<c type="type-two" id="3423"/>
<c type="type-three" id="9088"/>
</a>
y
is:
<b>
<c type="type-one" id="5675"/>
<c type="type-two" id="3423"/>
<c type="type-four" id="1234"/>
</b>
z
is:
<b>
<c type="type-three" id="3333"/>
<c type="type-four" id="1234"/>
</b>
Now, we can see that x ~ y
and y ~ z
. However, clearly this doesn't hold: x ~ z
This said, I am calling the relation "matches" and it is relaxed and not "equals".
Here is a solution to the problem, with the above adjustment:
Do note that this cannot be expressed with a single XPath expression, because XPath 1.0 (used within an XSLT 1.0 transformation) doesn't have range variables.
<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:template match="/*">
<xsl:call-template name="matches">
<xsl:with-param name="pElem1" select="a"/>
<xsl:with-param name="pElem2" select="b"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="matches">
<xsl:param name="pElem1" select="/.."/>
<xsl:param name="pElem2" select="/.."/>
<xsl:variable name="vMisMatch">
<xsl:for-each select="$pElem1/c[@type = $pElem2/c/@type]">
<xsl:if test=
"$pElem2/c[@type = current()/@type and not(@id = current()/@id)]">1</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:copy-of select="not(string($vMisMatch))"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following XML document:
<t>
<a>
<c type="type-one" id="5675"/>
<c type="type-two" id="3423"/>
<c type="type-three" id="9088"/>
</a>
<b>
<c type="type-one" id="5675"/>
<c type="type-two" id="3423"/>
</b>
</t>
the wanted, correct result is produced:
true
When the same transformation is applied on this XML document:
<t>
<a>
<c type="type-one" id="5675"/>
<c type="type-two" id="3423"/>
<c type="type-three" id="9088"/>
</a>
<b>
<c type="type-one" id="5675"/>
<c type="type-two" id="9876"/>
</b>
</t>
again the correct result is produced:
false
Here is what I came up with. Given a toy data set like this:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<a>
<item key="x" value="123"/>
<item key="y" value="456"/>
<item key="z" value="789"/>
</a>
<b>
<item key="x" value="123"/>
<item key="z" value="789"/>
</b>
</root>
This stylesheet shows how to test equality, as defined in the question.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" xmlns:set="http://exslt.org/sets" xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="set exsl">
<xsl:output method="text" version="1.0" encoding="UTF-8"/>
<xsl:template match="/">
<xsl:variable name="values-are-equal">
<xsl:call-template name="equal">
<xsl:with-param name="A" select="/root/a"/>
<xsl:with-param name="B" select="/root/b"/>
</xsl:call-template>
</xsl:variable>
<xsl:choose>
<xsl:when test="$values-are-equal = 1">Equal</xsl:when>
<xsl:otherwise>Inequal</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="equal">
<xsl:param name="A" />
<xsl:param name="B" />
<xsl:variable name="common-keys" select="$A/item/@key[ count(set:distinct( . | $B/item/@key )) = count( set:distinct( $B/item/@key ) ) ]"/>
<xsl:variable name="differences">
<xsl:for-each select="$common-keys">
<xsl:if test="$A/item[@key = current()]/@value != $B/item[@key = current()]/@value">
<different/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:choose>
<xsl:when test="count( exsl:node-set($differences)/* ) > 0">0</xsl:when>
<xsl:otherwise>1</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
This uses some extensions, which are available in xsltproc and other processors.