XML Tips and Tricks. Conditional XPath expressions

| 7 Comments | 1 TrackBack

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 or proposing better versions though), but I hope it would be interesting for the rest and may attract new readers.

Here is the first instalment - conditional XPath expressions.

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?
:)

Related Blog Posts

1 TrackBack

TrackBack URL: http://www.tkachenko.com/cgi-bin/mt-tb.cgi/158

XPath 1.0 notes for Sapphire XSLT from Confluence: Vaughn Harmon on March 18, 2009 2:37 PM

Sapphire uses XSLT 1.0 and XPath 1.0 for formatting comparision reports. XPath 1.0 doesn't support conditional expressions but there is a hack using the subsring function. see: Read More

7 Comments

Oh LOL, XSLT. I wondered where the conditionals were, and figured I must have been missing something obvious the XSLT Gods on W3C. Your web page confirmed what I suspected: XSLT is incomplete, but at least we can get around this with your string hack. Something I haven't had to do since the days of BASIC. Thanks for posting!

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

Juan, you can convert both to some common type (nodes or strings) and solve it using available methods.

Hello, great article!

Do you know how to choose between a number and a node-set?

$nodeset1[condition]|$number[not(condition)]

basically it's for do this:

precio*(unidades[../tipo='U']|1[not(../tipo='U')])
inside in a xsl:sort and in a xsl:value-of

where precio, unidades and tipo are node-set.
Could you response in the mail?

Thanx very much!

Juan Pedro
(Excuseme for my poor english):P

Yeah, you are right. I included number() only for readability and forgot to mention it's optonal in the face of * operator.
Thanks for valuable comments, Dimitre!

One more refinement to 1.:

One can use the constant:

1 div 0

to further simplify the conditional string selection:

concat(substring($s1, 1, $vInf*$p),
substring($s2, 1, $vInf*not($p))
)

Note that now you do not have to use string-length().


Cheers,

Dimitre Novatchev.

Just two things:

1. The explicit cast to number is not necessary and the conditional selection of a string is thus simplified to:

concat(substring($s1, 1, string-length($s1)*$p),
substring($s2, 1, string-length($s2)*not($p))
)

2. If one has to choose between two numbers, then the XPath expression is:

$n1*$p + $n2*not($p)


Cheers,

Dimitre Novatchev

Leave a comment