XSLT for-each with complex condition

26,158

Solution 1

Does this do what you want?

I drive the for-each selection on col 2 being true. That way position, which equals where we are in the selected set of nodes will equal 2 not 3

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0">

    <xsl:template match="/">
        <xsl:for-each select="clause/variable[@col='2' and text()='true']">
            <xsl:sort select="@row" data-type="number"/>
            <xsl:variable name="row-id" select="@row"/>
            <xsl:value-of select="concat(position(), ') ')"/>
            <xsl:value-of select="/clause/variable[@col='1' and @row=$row-id]"/>
        </xsl:for-each>        
    </xsl:template>

</xsl:stylesheet>

Though I'd probably use templates in preference to the for-each and use current() so that we don't need the row-id variable:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    version="1.0">

    <xsl:template match="/">
        <xsl:apply-templates select="clause/variable[@col='2' and text()='true']">
            <xsl:sort select="@row" data-type="number"/>            
        </xsl:apply-templates>        
    </xsl:template>

    <xsl:template match="clause/variable[@col='2' and text()='true']">
        <xsl:value-of select="concat(position(), ') ')"/>
        <xsl:value-of select="/clause/variable[@col='1' and @row=current()/@row]"/>
    </xsl:template>

</xsl:stylesheet>

Solution 2

I don't think you can express this with a single XPath 1.0 expression.

In XSLT 1.0 I will use keys and the solution becomes short, elegant and efficient:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output method="text"/>
 <xsl:key name="kVarCol2" match="variable[@col=2]" use="@row"/>

 <xsl:template match="/*">
    <xsl:for-each select="variable[@col='1'][key('kVarCol2', @row)='true']">
     <xsl:sort select="@row" data-type="number"/>
        <xsl:value-of select="concat('&#xA;', position(), ') ', .)"/>
    </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided XML document (corrected to be made well-formed):

<clause code="section6">
    <variable col="1" name="R1C1" row="1">Water</variable>
    <variable col="2" name="R1C2" row="1">true</variable>
    <variable col="1" name="R2C1" row="2">Gas</variable>
    <variable col="2" name="R2C2" row="2"></variable>
    <variable col="1" name="R3C1" row="3">Petrol</variable>
    <variable col="2" name="R3C2" row="3">true</variable>
</clause>

the wanted, correct result is produced:

1) Water
2) Petrol

II. XPath 2.0 (single expression) solution:

for $i in 1 to max(/*/*/@row/xs:integer(.))
  return
       /*/variable[@row eq string($i)]
            [@col eq '1'
           and
             ../variable
                 [@row eq string($i)
                and
                  @col eq '2'
                and
                  . eq 'true'
                 ]
            ]
             /concat('&#xA;', position(), ') ', .)

When this XPath 2.0 expression is evaluated on the same XML document (above), the result is the same wanted string:

1) Water 
2) Petrol
Share:
26,158
Admin
Author by

Admin

Updated on March 29, 2020

Comments

  • Admin
    Admin about 4 years

    I am transforming following XML to generate HTML.

    XML

    <clause code="section6">
          <variable col="1" name="R1C1" row="1">Water</variable>
          <variable col="2" name="R1C2" row="1">true</variable>
          <variable col="1" name="R2C1" row="2">Gas</variable>
          <variable col="2" name="R2C2" row="2"></variable>
          <variable col="1" name="R3C1" row="3">Petrol</variable>
          <variable col="2" name="R3C2" row="3">true</variable>
    <clause>
    

    XSLT

    1: <xsl:for-each select="$clause/variable[@col='1']">
    2:   <xsl:sort select="@row" data-type="number"/>
    3:   <xsl:variable name="row-id" select="@row"/>
    4:   <xsl:variable name="row" select="$clause/variable[@row=$row-id]"/>
    5:   <xsl:if test="$clause/variable[@col='2' and @row=$row-id]='true'">
    6:      <xsl:value-of name="row-no" select="concat(position(), ') ')"/>
    7:      <xsl:value-of select="$clause/variable[@col='1' and @row=$row-id]"/>
    8:   </xsl:if>
    9: </xsl:for-each>
    

    The transformation works fine and shows result 1) Water 3) Petrol

    The issue is sequence number. You can see condition on Line 5 filters rows that only have 'true' value in col 2 and position() used for displaying sequence number. I cannot have running counter in XLST.

    I was wondering if I can add condition of Line 5 with for-each at Line 1. The result with above example should be 1) Water 2) Patrol any advice?

  • Kevan
    Kevan about 12 years
    I still haven't got my head around XSLT 2. The code base I am maintaining all uses XSLT 1 even though we're using Saxon 9. I haven't found a good resource that says in 1 you used to do this - in 2 don't goes this do that - do you know of one?
  • Dimitre Novatchev
    Dimitre Novatchev about 12 years
    @Kevan: I remember recently there was such a question in the xslt tag and both Michael Kay and I answered it.