February 5, 2004

XML Tips and Tricks. Conditional XPath expressions

I'm introducing another category in my blog - XML Tips and Tricks, where I'm going to post some XML, XPath, XSLT, XML Schema, XQuery etc tips and tricks. I know, many of my readers being real XML gurus know all this stuff (I encourage to correct me when I'm wrong ...

If you are like me and addicted to write

return a>b? a : b;
instead of
if (a>b)
    return a;
else
    return b;
then you should be used to grumble programming in XSLT, because XPath 1.0 doesn't support conditional expressions (XPath 2.0 does though). The most notorious sample is when outputting a value into HTML table cell - you should assure it's not empty otherwise the cell will collapse into nothing in a browser. So one usually ends up with the following verbose pattern:
<xsl:choose>
    <xsl:when test="price != ''">
        <xsl:value-of select="price"/>
    </xsl:when>
    <xsl:otherwise>&#xA0;</xsl:otherwise>
</xsl:choose>
Or when you need to output some value or "n/a" string if the value is empty. Quite common requirements. Things get even worse when you need to set up a variable conditionally - the only way then is to nest xsl:choose switch within xsl:variable, thus getting result tree fragment instead of nodeset.

But in fact there are tricks to address this XPath 1.0 restriction. Here they are.

For conditional nodesets the trick formula is
$nodeset1[$condition] | $nodeset2[not($condition)]

It's an union of both nodesets, filtered by mutually exclusive conditional expressions. Easy to see than depending on boolean value of the $condition one nodeset will be selected and second one filtered out. E.g.

<xsl:variable name="var" select="//foo[$param] | //bar[not($param)]"/>
binds $var to //foo if $param is true and to //bar otherwise.

For conditional strings or numbers the trick formula is more complicted:
concat( substring($s1, number(not($condition))*string-length($s1)+1),
     substring($s2, number($condition)*string-length($s2)+1) )

While it looks quite convolute, the idea (Becker's method after Oliver Becker) is simple - in XPath number(true()) is 1, while number(false()) is 0 and when second argument of substring() function is greater than actual length of the string, empty string is returned. Hence substring($s1, number(not($condition))*string-length($s1)+1) returns $s1 if $condition is true and empty string otherwise. Concatenating two such expressions in mutually exclusive way gives us conditional strings expression.
There is also another variant:
concat( substring($s1, 1, number($condition)*string-length($s1)),
     substring($s2, 1, number(not($condition))*string-length($s2)) )

In practice such expressions can be great deal simplified though. For instance to output price if it's not empty or "n/a" otherwise one can use just

<xsl:value-of select="concat(price, 
     substring('n/a', (price!='')*string-length('n/a')+1))"/>

Another interesting trick is to leverage the ability of msxsl:node-set() (or exslt:node-set) extension function to convert a string into a text node, thus enabling using aforementioned conditional nodeset trick for strings too. Here is the same sample written using this method:

<xsl:value-of select="concat(price, 
     msxsl:node-set('n/a')[current()/price=''])"/>
Well, probably enough. Hope you had fun looking at this clumsy XPath tricks. Remember than it's the very first version of the XPath language after all and XPath 2.0 will make these tricks obsolete bringing in support for conditional expressions, such as
<xsl:value-of select="if ($part/@discounted) 
  then $part/wholesale 
  else $part/retail"/>
Till then it's good to know these tricks.


Quote of the day:

Is it possible to transform a XML document to another XML document using XSLT? How?
:)