tags:

views:

849

answers:

5

Given this XML data:

<root>
  <item>apple</item>
  <item>orange</item>
  <item>banana</item>
</root>

I can use this XSLT markup:

...
<xsl:for-each select="root/item">
  <xsl:value-of select="."/>,
</xsl:for-each>
...

to get this result:

apple, orange, banana,

but how do I produce a list where the last comma is not present? I assume it can be done doing something along the lines of:

...
<xsl:for-each select="root/item">
  <xsl:value-of select="."/>
  <xsl:if test="...">,</xsl:if>
</xsl:for-each>
...

but what should the test expression be?

I need some way to figure out how long the list is and where I currently am in the list, or, alternatively, if I am currently processing the last element in the list (which means I don't care how long it is or what the current position is).

+2  A: 
<xsl:if test="following-sibling::*">,</xsl:if>

or (perhaps more efficient, but you'd have to test):

<xsl:for-each select="*[1]">
   <xsl:value-of select="."/>
   <xsl:for-each select="following-sibling::*">
       <xsl:value-of select="concat(',',.)"/>
   </xsl:for-each>
</xsl:for-each>
Marc Gravell
Could you please elaborate on what that test is actually doing?
Anders Sandvig
the something:: indicates an axis - in this case, the following-sibling axis. There are various axes - the following-sibling axis is those nodes with the same parent that follow the current node in document order. This checks for the existence of any such nodes. If there aren't any, we're the last.
Marc Gravell
That works, but "position()=last()" doesn't have to build a node-set and then test it. The XSLT processor might not be smart enough to know that it isn't going to need to compile a list of every following node, and if it does, that makes it a (roughly) O(n^2) operation.
Robert Rossney
(Of course I meant "position() != last()". The devil is in the details.)
Robert Rossney
I'll post an alternative, then ;-p
Marc Gravell
+7  A: 

Take a look at the position(), count() and last() functions; e.g., test="position() &lt; last()".

Willie Wheeler
That's exactly what I was looking for, thanks! Where's stuff like this documented anyway? The XPath spec?
Anders Sandvig
Have a look at w3schools.com/xpath, they have a couple of great overviews of basic xpath syntax and capabilities.
Rahul
A: 

A simple XPath 1.0 one-liner:

     concat(., substring(',', 2 - (position() != last())))

Put it into this transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output method="text"/>

    <xsl:template match="/*">
      <xsl:for-each select="*">
        <xsl:value-of select=
         "concat(., substring(',', 2 - (position() != last())))"
         />
      </xsl:for-each>
    </xsl:template>
</xsl:stylesheet>

and apply it to the XML document:

<root>
    <item>apple</item>
    <item>orange</item>
    <item>banana</item>
</root>

to get the wanted result:

apple,orange,banana

EDIT:

Here is a comment from Robert Rossney to this answer:

That's pretty opaque code for a human to read. It requires you to know two non-obvious things about XSLT: 1) what the substring function does if its index is out of range and 2) that logical values can be implicitly converted to numerical ones.

and here is my answer:

Guys, never shy from learning something new. In fact this is all Stack Overflow is about, isn't it? :)

Dimitre Novatchev
That's pretty opaque code for a human to read. It requires you to know two non-obvious things about XSLT: 1) what the substring function does if its index is out of range and 2) that logical values can be implicitly converted to numerical ones.
Robert Rossney
@ Robert-Rossney Sure, part of its value is exactly in this!
Dimitre Novatchev
+1  A: 

This is a pretty common pattern:

<xsl:for-each select="*">
   <xsl:value-of select="."/>
   <xsl:if test="position() != last()>
      <xsl:text>,</xsl:text>
   </xsl:if>
</xsl:for-each>
Robert Rossney
I have updated my answer to reflect your comment to it :)
Dimitre Novatchev
+1  A: 

Robert gave the classis not(position() = last()) answer. This requires you to process the whole current node list to get context size, and in large input documents this might make the conversion consume more memory. Therefore, I normally invert the test to be the first thing

<xsl:for-each select="*">
  <xsl:if test="not(position() = 1)>, </xsl:if>
  <xsl:value-of select="."/>   
</xsl:for-each>
jelovirt
I'm trying to think of why you wouldn't want to do that, and not coming up with a reason.
Robert Rossney
position()!=last() won't work if you want exclude some of the items in the sequence. For instance what would happen if had a list like this:<list> <apple>delicious</apple> <banana>dole</banana> <orange>navel</orange></list>with the template <template match="orange"/> to exclude oranges.
BeWarned
@BeWarned, if you can't omit oranges in the select statement, then solutions with position() will not work. However, in this questions there are no template based exlucedes, so that doesn't really apply here.
jelovirt