Search code examples
xpathxsltxslt-1.0xpath-1.0libxslt

XPath 1.0 predicate with number function doesn't match all nonzero values in XSLT input


I'm using the xsltproc command and getting unexpected output from a predicate using the number() function. I'm trying to use number()'s output as a boolean, with 0 treated as false and all other values treated as true. The goal is to select all elements with nonzero @original attribute.

It's easiest to explain via a reproducer.

Reproducer:

  • Input
<top>
  <constraint>
    <lifetime>
      <rule id="rule1" original="1"/>
      <rule id="rule2" original="1"/>
    </lifetime>
  </constraint>
  <constraint>
    <lifetime>
      <rule id="rule3" original="3"/>
      <rule id="rule4" original="0"/>
      <rule id="rule5"/>
    </lifetime>
  </constraint>
</top>
  • Stylesheet
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:strip-space elements="*"/>
<xsl:output encoding="UTF-8" indent="yes" omit-xml-declaration="yes"/>

<xsl:template match="/|@*|node()" name="identity">
    <xsl:copy>
        <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
</xsl:template>

<xsl:template match="top">
    <xsl:for-each select="constraint/lifetime/rule[number(@original)]">
        <item>
            <xsl:copy-of select="."/>
        </item>
    </xsl:for-each>
</xsl:template>

</xsl:stylesheet>
  • Actual output
<item>
  <rule id="rule1" original="1"/>
</item>
  • Expected output
<item>
  <rule id="rule1" original="1"/>
</item><item>
  <rule id="rule2" original="1"/>
</item><item>
  <rule id="rule3" original="3"/>
</item>

rule1, rule2, and rule3 should be included in the output because their @original values are nonzero. rule4 and rule5 should be excluded because @original is 0 and unset, respectively.

If I change from number(@original) to number(@original) = 1 or @original = '1', I get the expected results. I also get the expected result if I use an if within the for-each instead of using an XPath predicate:

    <xsl:for-each select="constraint/lifetime/rule">
        <xsl:if test="number(@original)">
            <item>
                <xsl:copy-of select="."/>
            </item>
        </xsl:if>
    </xsl:for-each>

Edited to clarify the way I'm trying to use number() and to add rule elements that should not be included in the output, per feedback in the comments. My question has already been answered satisfactorily.


Solution

  • When a predicate expression evaluates to a number, it is considered to be true if the number is equal to the context position. Your expression:

    constraint/lifetime/rule[number(@original)]
    

    is equivalent to:

    constraint/lifetime/rule[position() = number(@original)]
    

    and therefore evaluates as true only for those elements that are the first rule child of their parent lifetime.

    See: https://www.w3.org/TR/1999/REC-xpath-19991116/#predicates