Thursday, 6 May 2010

Obtain position() from “for” expression in Xpath 2

One misunderstood and under utilized feature of XPath 2 is the for expression. The principal is fairly simple, whenever you have a sequence to iterate through, the “for” expression provides a simple mechanism to extract each of the sequences’ item() to a context variable and return something each time it changes context.

For those familiar with XQuery FLOWR expression, the “for” expression is very similar, but offers less features, as “let”, “order by” and “where” aren’t included in XPath 2.

One other feature of XQuery’s FLOWR expression, which is also missing from XPath 2, and I am providing a solution for in this blog is the “position” variable which can be useful at times, when you need to ad conditional statements to your return value, depending on the position of the item() in context.

Let’s first have a look at how you’d use a “for” expression in XSL.


<xsl:value-of select="for $i in (1 to 9) return $i"/>

returns: 1 2 3 4 5 6 7 8 9

Lets now use a pre-defined sequence of xs:string items instead, stored in a variable, and comma separate the items, but for the 2nd item where we also want to append the character “*”. We also want to separate the penultimate and the last item with an “and”, rather than a comma.

Because the “for” expression doesn’t provide any mechanism to keep track of context position, there isn’t a straight forward way to achieve the logic described above within the expression itself. Instead we’ll iterate the item count range, by creating a dynamic sequence based on the sequences’ item count.


<xsl:variable name="family" select="('Miguel','Louise','Merlyn','Kai','Eden')" as="xs:string*"/>

<xsl:value-of select="
for $i in (1 to count($family)) return
concat(
$family[$i],
if ($i = 2) then '*' else (),
if ($i = ( count($family) - 1)) then ' and'
else if ($i < (count($family) - 1)) then ','
else ''
)
"/>

Returns: Miguel, Louise*, Merlyn, Kai and Eden

Traditionally a “for” expression in XPath 2 would directly iterate through the sequence you’re planning to extract values from, but in this example, you’ll notice the “for” expression is iterating through a number sequence, consisting of the numbers 1 to the item count of the sequence held by the vatiable $family. ((1 to count($family)))

It is then possible to access each item of the $family variable, by dynamically providing the position of the item in the $family sequence, based on context. ($family[$i]).

You can then add various conditional statements to your return value, based on the value of $I which holds the context position, rather than the value you’re planning to output. ( if ($i = 2) then '*' else ())

The above example could be also achieved with an xsl:for-each which provides access to the position() function based on context.

<xsl:for-each select="$family">
<xsl:value-of select="."/>
<xsl:value-of select="
if (position() = (last() - 1)) then ' and '
else if(not(position() = last())) then ', '
else ''
"/>
</xsl:for-each>