XSL: Comparing nodes by comparing their child nodes - xslt

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.

Related

Checking multiple occurrences and manipulating them using XSLT

I have a requirement in which I need to check all four values of both occurrences in one condition and it is not given that hey1 and hello1 will come in one occurrence and hey2 and hello2 in the other.
I mean it is not guaranteed that hey1 or hey2 come in as first or second; the same applies for hello1 and hello2.
I am using the below code which gives me the output <output>ININ</output>, but I need it as <output>IN</output>.
I am trying this POC and I have a sample XML below:
<q>
<a>
<b>hey1</b>
<c>hello1</c>
</a>
<a>
<b>hey2</b>
<c>hello2</c>
</a>
</q>
Kindly provide me a solution which checks all four conditions and in which one if/when condition used for each <a> will occur twice or multiple times.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:output encoding="UTF-8" version="1.0" method="xml" indent="yes"/>
<xsl:template match="/">
<xsl:variable name="a" select="/q/a"/>
<output>
<xsl:for-each select="$a">
<xsl:if test="$a/b='hey1' and $a/c= 'hello1' and $a/b='hey2' and $a/c= 'hello2'">
<xsl:value-of select="'IN'"/>
</xsl:if>
</xsl:for-each>
</output>
</xsl:template>
</xsl:stylesheet>
I hope that I my question is clear enough to answer it.
Thank you for any advice.
Try:
<xsl:template match="q">
<xsl:if test="a[b='hey1' and c='hello1'] and a[b='hey2' and c='hello2'] ">
<xsl:text>IN</xsl:text>
</xsl:if>
</xsl:template>
This will return "IN" for both:
<q>
<a>
<b>hey1</b>
<c>hello1</c>
</a>
<a>
<b>hey2</b>
<c>hello2</c>
</a>
</q>
and:
<q>
<a>
<b>hey2</b>
<c>hello2</c>
</a>
<a>
<b>hey1</b>
<c>hello1</c>
</a>
</q>
but not for:
<q>
<a>
<b>hey1</b>
<c>hello2</c>
</a>
<a>
<b>hey2</b>
<c>hello1</c>
</a>
</q>

XSL Grouping and sub grouping

I found this post on Muenchian grouping XSLT 1.0 Group By, and got part way with what I want to do, but I can't work out how to group my sub-nodes.
My XML looks a little like this:
<NewDataSet>
<Vehicle>
<ManufacturerId>53</ManufacturerId>
<ManufacturerName>VAUXHALL</ManufacturerName>
<Model>Corsa</Model>
</vehicle>
<Vehicle>
<ManufacturerId>53</ManufacturerId>
<ManufacturerName>VAUXHALL</ManufacturerName>
<Model>Astra</Model>
</vehicle>
<Vehicle>
<ManufacturerId>53</ManufacturerId>
<ManufacturerName>VAUXHALL</ManufacturerName>
<Model>Corsa</Model>
</vehicle>
<Vehicle>
<ManufacturerId>54</ManufacturerId>
<ManufacturerName>FORD</ManufacturerName>
<Model>KA</Model>
</vehicle>
<Vehicle>
<ManufacturerId>54</ManufacturerId>
<ManufacturerName>FORD</ManufacturerName>
<Model>Focus</Model>
</vehicle>
<Vehicle>
<ManufacturerId>54</ManufacturerId>
<ManufacturerName>FORD</ManufacturerName>
<Model>KA</Model>
</vehicle>
<Vehicle>
<ManufacturerId>55</ManufacturerId>
<ManufacturerName>CITROEN</ManufacturerName>
<Model>C4</Model>
</vehicle>
<NewDataSet>
This is the code I have thus far
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" omit-xml-declaration="yes" />
<xsl:key name="groups" match="/NewDataSet/Vehicle" use="ManufacturerName" />
<xsl:template match="/NewDataSet">
<xsl:apply-templates select="Vehicle[generate-id() = generate-id(key('groups', ManufacturerName)[1])]"/>
</xsl:template>
<xsl:template match="Vehicle">
<div class="makeLnk">
<xsl:value-of select="ManufacturerName"/>
<xsl:for-each select="key('groups', ManufacturerName)">
<div class="modelLnk"><xsl:value-of select="Model"/></div>
</xsl:for-each>
</div>
</xsl:template>
</xsl:stylesheet>
This groups together the ManufacturerNames and lists all the models under. But I would now like to group the models as well, thus removing duplicate names. But can't work out the syntax.
Would appreciate any help.
Thanks.
Use a second key composed of the ManufacturerName of the Vehicle and the value of the Model:
<xsl:key name="model" match="Vehicle/Model" use="concat(../ManufacturerName, '|', .)"/>
then replace
<xsl:template match="Vehicle">
<div class="makeLnk">
<xsl:value-of select="ManufacturerName"/>
<xsl:for-each select="key('groups', ManufacturerName)">
<div class="modelLnk"><xsl:value-of select="Model"/></div>
</xsl:for-each>
</div>
</xsl:template>
with
<xsl:template match="Vehicle">
<div class="makeLnk">
<xsl:value-of select="ManufacturerName"/>
<xsl:for-each select="key('groups', ManufacturerName)/Model[generate-id() =
generate-id(key('model', concat(../ManufacturerName, '|', .))[1])]">
<div class="modelLnk"><xsl:value-of select="."/></div>
</xsl:for-each>
</div>
</xsl:template>

How to group elements in xsl1.0 and 2.0?

Such an example
<?xml version="1.0" encoding="UTF-8"?>
<root>
<a id='a1' name='a1 is a Chinese pig'/>
<b text='b1'/>
<d test='test0' location='L0' text='c0'/>
<a id='a2' name='a2 is a Japanese pig'/>
<b text='b2'/>
<c test='test1' location='L1' text='c1 is a red pig'/>
<c test='test2' location='L2' text='c2 is a green pig'/>
<a id='a3' name='a3 is a American dog'/>
<b text='b3'/>
<c test='test3' location='L3' text='c3 is a lovely dog'/>
<c test='test4' location='L4' text='c4 is a ugly dog'/>
<c test='test5' location='L5' text='c5 is a smart dog'/>
<a id='a4' name='a4 is a Japanese bird'/>
<b text='b4'/>
<c test='test6' location='L6' text='c6 is a lovely bird'/>
<c test='test7' location='L7' text='c7 is a ugly bird'/>
<c test='test8' location='L8' text='c8 is a smart bird'/>
<a id='a5' name='a5 is a American pig'/>
<b text='b2'/>
<c test='test10' location='L10' text='c10 is a red pig'/>
<c test='test11' location='L11' text='c11 is a green pig'/>
<a id='a6' name='a6 is a Chinese dog'/>
<b text='b3'/>
<c test='test12' location='L12' text='c12 is a lovely dog'/>
<c test='test14' location='L14' text='c14 is a ugly dog'/>
<c test='test15' location='L15' text='c15 is a smart dog'/>
<a id='a7' name='a7 is a Chinese bird'/>
<b text='b4'/>
<c test='test16' location='L16' text='c16 is a lovely bird'/>
<c test='test17' location='L17' text='c17 is a ugly bird'/>
<c test='test18' location='L18' text='c18 is a smart bird'/>
</root>
I apply a xsl1.0 like this
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes"/>
<xsl:key name="lookup" match="c" use="generate-id(preceding-sibling::a[1])" />
<xsl:template match="/root">
<xsl:apply-templates select="a[key('lookup', generate-id())]" />
</xsl:template>
<xsl:template match="a">dog:<xsl:value-of select="concat(#name[contains(.,'dog')], ':
', '
')" />
<xsl:apply-templates select="key('lookup', generate-id())" />
</xsl:template>
<xsl:template match="c">
<xsl:apply-templates select="#*" />
<xsl:value-of select="'
'" />
</xsl:template>
<xsl:template match="c/#*">
<xsl:value-of select="concat(local-name(), ':', ., ':
')" />
</xsl:template>
</xsl:stylesheet>
I want a output like this,the rule is
if there is one or more following c elements between a element and the next a , then we ouput these c elements
then according to the #name, check that value contain some string like pig,dog or bird ,these are key words, not just a hard code work here. we could group these information ,because there are more pigs,birds and dogs.not matter they are chinese or japanese or else,we group according to the dogs,bird ,pig these category we know before.
The output I want is like this
case1:the following are all pigs:
a2 is a Japanese pig
test:test1
location:L1
text:c1 is a red pig
test:test2
location:L2
text:c2 is a green pig
a5 is a American pig
test:test10
location:L10
text:c10 is a red pig
test:test11
location:L11
text:c11 is a green pig
case2:the following are all dogs:
a3 is a American dog
test:test3
location:L3
text:c3 is a lovely dog
test:test4
location:L4
text:c4 is a ugly dog
test:test5
location:L5
text:c5 is a smart dog
a6 is a Chinese dog
test:test12
location:L12
text:c12 is a lovely dog
test:test14
location:L14
text:c14 is a ugly dog
test:test15
location:L15
text:c15 is a smart dog
case3:the following are all birds:
a4 is a Japanese bird
test:test6
location:L6
text:c6 is a lovely bird
test:test7
location:L7
text:c7 is a ugly bird
test:test8
location:L8
text:c8 is a smart bird
a7 is a Chinese bird
test:test16
location:L16
text:c16 is a lovely bird
test:test17
location:L17
text:c17 is a ugly bird
test:test18
location:L18
text:c18 is a smart bird
The question is how to correct my xsl1.0? Another is this more easy to realize in xsl2.0?
with this xsl
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes"/>
<xsl:template match="/root">
<p>the followings are birds:</p>
<xsl:apply-templates select="a[contains(#name, 'bird')][key('bird_lookup', generate-id())]"/>
<p>the followings are pigs:</p>
<xsl:apply-templates select="a[contains(#name, 'pig')][key('pig_lookup', generate-id())]"/>
<p>the followings are dogs:</p>
<xsl:apply-templates select="a[contains(#name, 'dog')][key('dog_lookup', generate-id())]"/>
</xsl:template>
<xsl:key name="bird_lookup" match="c" use="generate-id(preceding-sibling::a[1][contains(#name, 'bird')])"/>
<xsl:template name="bird" match="a[contains(#name, 'bird')]">
<xsl:value-of select="'
'"/>
<xsl:value-of select="#name"/>
<xsl:value-of select="'
'"/>
<xsl:apply-templates select="key('bird_lookup', generate-id())" />
<xsl:apply-templates select="c/#*"/>
</xsl:template>
<xsl:key name="pig_lookup" match="c" use="generate-id(preceding-sibling::a[1][contains(#name, 'pig')])"/>
<xsl:template name="pig" match="a[contains(#name, 'pig')]">
<xsl:value-of select="'
'"/>
<xsl:value-of select="#name"/>
<xsl:value-of select="'
'"/>
<xsl:apply-templates select="key('pig_lookup', generate-id())" />
<xsl:apply-templates select="c/#*"/>
</xsl:template>
<xsl:key name="dog_lookup" match="c" use="generate-id(preceding-sibling::a[1][contains(#name, 'dog')])"/>
<xsl:template name="dog" match="a[contains(#name, 'dog')]">
<xsl:value-of select="'
'"/>
<xsl:value-of select="#name"/>
<xsl:value-of select="'
'"/>
<xsl:apply-templates select="key('dog_lookup', generate-id())" />
<xsl:apply-templates select="c/#*"/>
</xsl:template>
<xsl:template match="c">
<xsl:apply-templates select="#*"/>
<xsl:value-of select="'
'"/>
</xsl:template>
<xsl:template match="c/#*">
<xsl:value-of select="concat(local-name(), ':', ., '
')"/>
</xsl:template>
</xsl:stylesheet>
i can get
the followings are birds:
a4 is a Japanese bird
test:test6
location:L6
text:c6 is a lovely bird
test:test7
location:L7
text:c7 is a ugly bird
test:test8
location:L8
text:c8 is a smart bird
a7 is a Chinese bird
test:test16
location:L16
text:c16 is a lovely bird
test:test17
location:L17
text:c17 is a ugly bird
test:test18
location:L18
text:c18 is a smart bird
the followings are pigs:
a2 is a Japanese pig
test:test1
location:L1
text:c1 is a red pig
test:test2
location:L2
text:c2 is a green pig
a5 is a American pig
test:test10
location:L10
text:c10 is a red pig
test:test11
location:L11
text:c11 is a green pig
the followings are dogs:
a3 is a American dog
test:test3
location:L3
text:c3 is a lovely dog
test:test4
location:L4
text:c4 is a ugly dog
test:test5
location:L5
text:c5 is a smart dog
a6 is a Chinese dog
test:test12
location:L12
text:c12 is a lovely dog
test:test14
location:L14
text:c14 is a ugly dog
test:test15
location:L15
text:c15 is a smart dog
how to make this elegant?

Apply template on preceding-sibling of following sibling of current node

Hi I have following Input
<Root>
<A rename="yes"/>
<B rename="no"/>
<C rename="no"/>
<D rename="yes"/>
<E rename="no"/>
<F all="yes"/>
</Root>
Currently i am at <A> and i want apply template on the element whose #rename="yes", that is coming before element <F>.
i am trying to doing something like:
<xsl:apply-templates select=
"following-sibling::*[#all='yes']/preceding-sibling::node()[#rename='yes'" />
But i am not getting the expected output. Please suggest.
Currently i am at <A> and i want
apply template on the element whose
#rename="yes", that is coming before
element <F>
You want this XPath expression (assuming A has only one following sibling named F):
following-sibling::F/preceding-sibling::*[#rename='yes'][1]
It selects any element whose rename attribute has value of "yes" and that is the first such preceding sibling of any following sibling (of the current node) element named F.
Here is a complete XSLT transformation:
<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="A">
<xsl:apply-templates mode="found" select=
"following-sibling::F/preceding-sibling::*[#rename='yes'][1]"/>
</xsl:template>
<xsl:template match="*" mode="found">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<Root>
<A rename="yes"/>
<B rename="no"/>
<C rename="no"/>
<D rename="yes"/>
<E rename="no"/>
<F all="yes"/>
</Root>
the wanted, correct result is produced:
<D rename="yes"/>

Conditional Auto increment in xsl

I have an XML like this:
<V>
<W>
<X>1</X>
</W>
<W>
<Y>1</Y>
</W>
<W>
<X>1555</X>
</W>
<W>
<X>1</X>
</W>
</V>
I want to make it something like this:
<entity ID="start">
<f ID="NewField">0001</f>
<f ID="NewField">0001</f>
<f ID="NewField">0002</f>
<f ID="NewField">0003</f>
</entity>
When the field is V/W/X then NewField should be incremented by 1 as many times the tag V/W/X is found.
Similarly for V/W/Y.
The XSL which I am using is
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<entity ID="start">
<xsl:for-each select="V/W">
<xsl:if test="X">
<xsl:variable name="my_var">
<xsl:value-of select="concat('000',position())"/>
</xsl:variable>
<f ID="NewField"><xsl:value-of select="$my_var"/></f>
</xsl:if>
<xsl:if test="Y">
<xsl:variable name="my_var">
<xsl:value-of select="concat('000',position())"/>
</xsl:variable>
<f ID="NewField"><xsl:value-of select="$my_var"/></f>
</xsl:if>
</xsl:for-each>
</entity>
</xsl:template>
</xsl:stylesheet>
but it gives me a wrong result, something like this:
<entity ID="start">
<f ID="NewField">0001</f>
<f ID="NewField">0002</f>
<f ID="NewField">0003</f>
<f ID="NewField">0004</f>
</entity>
If you want to number nodes with XSLT then the xsl:number element can help:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output indent="yes"/>
<xsl:template match="/">
<entity ID="start">
<xsl:apply-templates select="descendant::X | descendant::Y"/>
</entity>
</xsl:template>
<xsl:template match="X | Y">
<f ID="NewField"><xsl:number level="any" format="0000"/></f>
</xsl:template>
</xsl:stylesheet>
I think you're looking for something like count(preceding::X) expression. Of course you may want to make it more complex and then take care about number formatting, but that sounds like a starting point you're looking for.
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:template match="V">
<entity ID="start">
<xsl:apply-templates select="W/X|W/Y" />
</entity>
</xsl:template>
<xsl:template match="X|Y">
<f ID="NewField">
<xsl:variable name="counter" select="
count(
parent::W/preceding-sibling::W/*[name() = name(current())]
) + 1
" />
<xsl:value-of select="format-number($counter, '0000')" />
</f>
</xsl:template>
</xsl:stylesheet>
This:
parent::W/preceding-sibling::W/*[name() = name(current())]
selects all preceding elements of the same name as the current element. E.g., if the point of execution is on this node:
<X>1555</X>
It goes one level up (parent::W), then selects all preceding <W> siblings, and of those it selects any child (*) that has a name of X - since X is the name of the current() element.
The resulting node-set is counted and incremented by one. format-number() is used to generate a nice clean output:
<entity ID="start">
<f ID="NewField">0001</f>
<f ID="NewField">0001</f>
<f ID="NewField">0002</f>
<f ID="NewField">0003</f>
</entity>