2

Assuming I have an xml.document like this (not complete):

<root>
<elem id="1"/>
<elem id="2"/>
<elem id="3"/>
</root>

and I access it via the doc-function, like this:

<xsl:variable name="curDoc" select="doc(iri-to-uri("abc.xml"))"/>

now I want to compare each element in the document that I apply the stylesheet to with each element from that abc.xml.

The document I apply the stylesheet to looks like this:

<node>
<somenode id="1"/>
<somenode id="2"/>
<somenode id="3"/>
</node>

Now the elements in both documents are actually not in order!

Is there another way to do this than the following:

<xsl:template match="somenode">
<xsl:variable name="curElem" select="."/>
<xsl:for-each select="$curDoc/root/elem">
<xsl:if test="@id = $curElem[@id]">
<!-- do something -->
<xsl:if>
</xsl:for-each>
</xsl:template>

I read a lot that you should avoid for-each and just use the template mechanism, but how could I do this here without for-each (using XSLT 2.0)?

user3629892
  • 2,960
  • 9
  • 33
  • 64
  • 2
    `xsl:for-each` doesn't always have to be avoided, it should not be treated as if it's a 'for' loop in a procedural language context – Dan Field Mar 30 '15 at 15:15
  • @DanField In this case though it can easily be avoided. – biziclop Mar 30 '15 at 15:16
  • 3
    See http://stackoverflow.com/a/4462752/423105 on why for-each is sometimes better than apply-templates. It exists for a good reason, and simply avoiding it all the time is not always a "best practice." Even better, read Jeni Tennison: http://www.jenitennison.com/2007/05/01/matching-templates-named-templates-or-for-each-my-rules-of-thumb.html – LarsH Mar 30 '15 at 15:17
  • @LarsH That's fair enough, the only thing I wouldn't agree with is that `for-each` is easier to read. And it's just as easy to make fragile assumptions with for-each too. However I agree you shouldn't avoid it for avoidance's sake. – biziclop Mar 30 '15 at 15:23
  • 1
    @biziclop I'm happy to discuss this but I think the discussion belongs on http://stackoverflow.com/questions/4460232/differences-between-for-each-and-templates-in-xsl/4462752#4462752 – LarsH Mar 30 '15 at 15:24
  • 2
    What is exactly is the purpose of these comparisons? It's quite likely it could be achieved without going over each possible combination of two nodes - regardless of how *that* would be done, be it a template or a for-each. – michael.hor257k Mar 30 '15 at 15:41

4 Answers4

2

Move the condition into the <for-each> select.

<xsl:template match="somenode">
  <xsl:variable name="currentElementId" select="./@id"/>
  <xsl:for-each select="$curDoc/root/elem[@id = $currentElementId]">
    <!-- do something -->
  </xsl:for-each>
</xsl:template>

This solution still uses the loop. It is correct because the node list could be empty or you could have several elem elements with the same id attribute value.

In XML the attribute id is not necessarily unique, it depends on the document definition (DTD, XSD, ...).

Most functions use the first node in a list so it should work without the for-each, actually.

<xsl:template match="somenode">
  <xsl:variable name="currentElementId" select="./@id"/>
  <xsl:variable name="targetNode" select="$curDoc/root/elem[@id = $currentElementId]">
  <xsl:if test="$targetNode">
    <!-- do something with $targetNode -->
  </xsl:if>
</xsl:template>
ThW
  • 19,120
  • 3
  • 22
  • 44
2

xsl:for-each is not intrinsically evil. It's just that excessive use of xsl:for-each (and xsl:if) is often a symptom of a programmer who hasn't mastered the more powerful constructs in the language.

Looking at your code, you are actually doing a join, and the best way to do a join in XSLT is to use keys. So I would write this as

<xsl:key name="id" match="elem" use="@id"/>

<xsl:template match="somenode">
  <xsl:for-each select="key('id', @id, $curdoc)">
    <!-- do something -->
  </xsl:for-each>
</xsl:template>

You could replace the xsl:for-each with an xsl:apply-templates, perhaps in a specific mode, but I wouldn't do it unless there is some reason, e.g. an expectation that someone might want one day to substitute your "do something" with "do something different".

Michael Kay
  • 156,231
  • 11
  • 92
  • 164
0

Here is a small example how what you want can be done without using an <xsl:for-each> instruction:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:variable name="vDoc">
        <root>
            <elem id="1"/>
            <elem id="2"/>
            <elem id="3"/>
        </root> 
 </xsl:variable>

  <xsl:template match="somenode">
    <xsl:apply-templates select="$vDoc/*/elem[@id eq current()/@id]">
      <xsl:with-param name="porigElem" select="."/>
    </xsl:apply-templates>
  </xsl:template>

  <xsl:template match="elem">
    <xsl:param name="porigElem" as="element()"/>
    Match:
      Original: <xsl:sequence select="$porigElem"/>
      Matching: <xsl:sequence select="."/>
  </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided source XML document (reordered, to match the description of the problem):

<node>
    <somenode id="3"/>
    <somenode id="1"/>
    <somenode id="2"/>
</node>

The wanted, correct result is produced:

Match:
  Original: <somenode id="3"/>
  Matching: <elem id="3"/>

Match:
  Original: <somenode id="1"/>
  Matching: <elem id="1"/>

Match:
  Original: <somenode id="2"/>
  Matching: <elem id="2"/>

Of course, if one wants to use keys, as recommended in the answer of Dr. Kay, the key must be defined, and then the applying of templates will look like this:

<xsl:apply-templates select="key('kelemById', @id, $vDoc)">
  <xsl:with-param name="porigElem" select="."/>
</xsl:apply-templates>

The complete transformation now is:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:key name="kelemById" match="elem" use="@id"/>

 <xsl:variable name="vDoc">
        <root>
            <elem id="1"/>
            <elem id="2"/>
            <elem id="3"/>
        </root> 
 </xsl:variable>

  <xsl:template match="somenode">
    <xsl:apply-templates select="key('kelemById', @id, $vDoc)">
      <xsl:with-param name="porigElem" select="."/>
    </xsl:apply-templates>
  </xsl:template>

  <xsl:template match="elem">
    <xsl:param name="porigElem" as="element()"/>
    Match:
      Original: <xsl:sequence select="$porigElem"/>
      Matching: <xsl:sequence select="."/>
  </xsl:template>
</xsl:stylesheet>

and when this transformation is applied on the same source XML document, the same wanted and correct result is produced.

Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
-1

i'm not sure if this is nicer, but that should work:

<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

  <xsl:template match="somenode">
     <xsl:variable name="curElem" select="."/>
     <xsl:apply-templates select="$curDoc" />
  </xsl:template>

  <xsl:template match="root/elem">
        <xsl:if test="@id = $curElem[@id]">
           test
        </xsl:if>
  </xsl:template>

guthy
  • 326
  • 1
  • 4