Re: [xsl] concatenation of sibling names

Subject: Re: [xsl] concatenation of sibling names
From: Jeni Tennison <jeni@xxxxxxxxxxxxxxxx>
Date: Tue, 12 Feb 2002 18:03:08 +0000
Hi Saverio,

> Is it possible to concatenate the names of all siblings of a node
> into a string for later use?

It depends what you mean by "for later use". You can assign a variable
a string that is the concatenation of the names of all siblings of a
node fairly easily - whether you can use that variable at the location
you want to use the variable depends on where you create the variable
and where you want to use the variable. You can't, for example, create
the variable in one template and then use it in another template.
(Though you can pass the value of the variable through to a later
template explicitly by using parameters.)

To get all the siblings of a node, you can create a union of its
preceding siblings and its following siblings:

  preceding-sibling::* | following-sibling::*

You can iterate over those with an xsl:for-each, and for each of them
give their name:

  <xsl:for-each select="preceding-sibling::* | following-sibling::*">
    <xsl:value-of select="name()" />
  </xsl:for-each>

Actually, it looks like you want the names to be separated by spaces,
so you need to add a space after all but the last name:

  <xsl:for-each select="preceding-sibling::* | following-sibling::*">
    <xsl:value-of select="name()" />
    <xsl:if test="position() != last()"> </xsl:if>
  </xsl:for-each>

You can assign the result of that xsl:for-each to a variable by
including the xsl:for-each within the variable:

  <xsl:variable name="sibling-names">
    <xsl:for-each select="preceding-sibling::* | following-sibling::*">
      <xsl:value-of select="name()" />
      <xsl:if test="position() != last()"> </xsl:if>
    </xsl:for-each>
  </xsl:variable>

Technically, this variable holds a result tree fragment, but if you
use it like a string then it will behave in the same way as a string,
so that technicality doesn't matter much in practice.

---

According to the WDs, in XSLT 2.0, you could do:

  <xsl:variable name="sibling-names">
    <xsl:value-of separator=" "
      select="for $s in (preceding-sibling::* | following-sibling::*)
              return name($s)" />
  </xsl:variable>

Oddly enough, I can't think of a way to explicitly create a string in
one easy move, however. (And this is a bit more important in XSLT 2.0,
where data types do really matter.) What you'd expect is a
concat-sequence() function, taking a sequence of strings and returning
the concatenation of those strings. Although actually things wouldn't
be particularly pretty even if you did have such a thing... I *think*
you would have to do one of the following:

  <!-- create a text node holding the string, using xsl:value-of and
       it's helpful separator attribute -->
  <xsl:variable name="sibling-names-text-node">
    <xsl:value-of separator=" "
      select="for $s in (preceding-sibling::* | following-sibling::*)
              return name($s)" />
  </xsl:variable>
  <!-- convert the text node to a string with the string() function
       -->
  <xsl:variable name="sibling-names"
                select="string($sibling-names-text-node)" />
  
Or:

  <!-- create a sequence of, for each sibling, their name followed by
       a space; take all but the last item in that sequence, and
       concatenate the items together into a string with the
       (putative) concat-sequence() function -->
  <xsl:variable name="sibling-names"
    select="concat-sequence(
              (for $s in (preceding-sibling::* | following-sibling::*)
               return (name($s), ' '))
                 [position() != last()])" />
              
Or:

  <!-- hold the siblings in a variable to make the processing easier
       -->
  <xsl:variable name="siblings"
    select="preceding-sibling::* | following-sibling::*" />
  <!-- create the sibling names by concatenating together the items in
       the sequence consisting of all the names of the siblings
       followed by a space (or an empty string for the last of the
       siblings) -->
  <xsl:variable name="sibling-names"
    select="concat-sequence(
              for $i in 1 to count($siblings)
              return (name($siblings[$i]),
                      if ($i != count($siblings))
                      then ' ' else '')" />

The first is messy, because you have to do it in two steps. The second
is all right, but a little counter-intuitive. The third demonstrates
how tricky it is to iterate over a sequence with the for expression
(as currently defined) if the position of an item in the expression is
actually an important piece of information.

The idea of a for expression that uses the context node rather than a
range variable (or a simple mapping operator) really helps in this
situation. If you *didn't* have range variables, it would limit the
flexibility of the for expression (because you couldn't do joins), but
it would make this task a lot easier because you could do:

  <xsl:variable name="sibling-names"
    select="concat-sequence(
              for (preceding-sibling::* | following-sibling::*)
              return (name(),
                      if (position() != last()) then ' ' else ''))" />

The changes to XSLT that I favour would (a) use a simple mapping
operator rather than a for expression [it's just a different syntax,
but I think it's a handy one] and (b) enable you to assign values
other than (new) node sets to variables using their content. Ideally,
I'd like it if you could make $sibling-names hold a string with just:

  <xsl:variable name="sibling-names" type="xs:string">
    <xsl:value-of separator=" "
      select="(preceding-sibling::* | following-sibling::*)
                => name()" />
  </xsl:variable>

But in any case, it sounds like a concat-sequence() function would be
useful.

Cheers,

Jeni

---
Jeni Tennison
http://www.jenitennison.com/


 XSL-List info and archive:  http://www.mulberrytech.com/xsl/xsl-list


Current Thread