XSLT concat string, remove last comma
Asked Answered
C

6

29

I need to build up a string using XSLT and separate each string with a comma but not include a comma after the last string. In my example below I will have a trailing comma if I have Distribution node and not a Note node for instance. I don't know of anyway to build up a string as a variable and then truncate the last character in XSLT. Also this is using the Microsoft XSLT engine.

My String =

<xsl:if test="Locality != ''">
  <xsl:value-of select="Locality"/>,
</xsl:if>
<xsl:if test="CollectorAndNumber != ''">
  <xsl:value-of select="CollectorAndNumber"/>,
</xsl:if>
<xsl:if test="Institution != ''">
  <xsl:value-of select="Institution"/>,
</xsl:if>
<xsl:if test="Distribution != ''">
  <xsl:value-of select="Distribution"/>,
</xsl:if>
<xsl:if test="Note != ''">
  <xsl:value-of select="Note"/>
</xsl:if>

[Man there's gotta be a better way to enter into this question text box :( ]

Charissacharisse answered 28/4, 2009 at 14:29 Comment(2)
@Craig: I personally find the SO text editor to be the best one I've ever used (web based, at least). What don't you like about it?Andiron
@Weblog I generally agree with you. One exception is a nasty bug, when the SO code editor thinks predicates in XPath (like myElem[1]) are hyperlinks and changes them to something like myElem[5], depending on how many real links are already there in the text.Bounds
B
53

This is very easy to accomplish with XSLT (No need to capture the results in a variable, or to use special named templates):

I. XSLT 1.0:

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

    <xsl:template match="/*/*">
      <xsl:for-each select=
      "Locality/text() | CollectorAndNumber/text()
     | Institution/text() | Distribution/text()
     | Note/text()
      "
      >
        <xsl:value-of select="."/>
        <xsl:if test="not(position() = last())">,</xsl:if>
      </xsl:for-each>
    </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the following XML document:

<root>
    <record>
        <Locality>Locality</Locality>
        <CollectorAndNumber>CollectorAndNumber</CollectorAndNumber>
        <Institution>Institution</Institution>
        <Distribution>Distribution</Distribution>
        <Note></Note>
        <OtherStuff>Unimportant</OtherStuff>
    </record>
</root>

the wanted result is produced:

Locality,CollectorAndNumber,Institution,Distribution

If the wanted elements should be produced not in document order (something not required in the question, but raised by Tomalak), it is still quite easy and elegant to achieve this:

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

    <xsl:param name="porderedNames"
     select="' CollectorAndNumber Locality Distribution Institution Note '"/>

    <xsl:template match="/*/*">
        <xsl:for-each select=
         "*[contains($porderedNames, concat(' ',name(), ' '))]">

         <xsl:sort data-type="number"
          select="string-length(
                     substring-before($porderedNames,
                                      concat(' ',name(), ' ')
                                      )
                                )"/>

            <xsl:value-of select="."/>
            <xsl:if test="not(position() = last())">,</xsl:if>
        </xsl:for-each>
    </xsl:template>
</xsl:stylesheet>

Here the names of the wanted elements and their wanted order are provided in the string parameter $porderedNames, which contains a space-separated list of all wanted names.

When the above transformation is applied on the same XML document, the wanted result is produced:

CollectorAndNumber,Locality,Distribution,Institution

II. XSLT 2.0:

In XSLT this task is even simpler (again, no special function is necessary):

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

    <xsl:template match="/*/*">
    <xsl:value-of separator="," select=
    "(Locality, CollectorAndNumber,
     Institution, Distribution,
     Note)[text()]" />
    </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the same XML document, the same correct result is produced:

Locality,CollectorAndNumber,Institution,Distribution

Do note that the wanted elements will be produced in any desired order, because we are using the XPath 2.0 sequence type (vs the union in the XSLT 1.0 solution), which by definition contains items in any desired (specified) order.

Bounds answered 28/4, 2009 at 16:22 Comment(5)
Would this not imply document order in the XSLT 1.0 output? I deliberately used the variable to have the freedom of ordering the output the way I like it.Venicevenin
@Venicevenin This is not clear from the question, and in the provided XML document the document order matches the wanted order. If a different ordering of the output were needed, do note that the XSLT 2.0 solution does not rely on document order. For a document-order-independent XSLT 1.0 solution I would prefer to be given the list of element names in some parameter -- then it is still possible to have a single <xsl:for-each> or <xsl:template. However, this goes to far beyond the original question. I would prefer a separate question on this and then I'll provide the described solution.Bounds
I'm stuck with XSLT 1.0 but you figured it out. Thanks. An improvement on your method would be to modify the IF to be '<xsl:if test="(. != '') and (position() != last())">'Charissacharisse
@Charissacharisse Actually, this is not needed. the test for text() does not succeed if the text is of zero length.Bounds
@Dimitre Novatchev: Though custom order was not specified in the original question, I think this is not too far-fetched. In fact, both your 1.0 solutions are neat. I would have posted your XSLT 1.0 solution #1 myself (it's obvious, after all), but document order independence was important to me. +1 for the XSLT 1.0 solution #2 and the XSLT 2.0 variant.Venicevenin
S
7

I would prefer a short call-template to join the node values together. This also works if a node in the middle of your concatenated list, e.g. Institution, is missing:

<xsl:template name="join">
    <xsl:param name="list" />
    <xsl:param name="separator"/>

    <xsl:for-each select="$list">
        <xsl:value-of select="." />
        <xsl:if test="position() != last()">
            <xsl:value-of select="$separator" />
        </xsl:if>
    </xsl:for-each>
</xsl:template>

Here is a short example how to use it:

Sample input document:

<?xml version="1.0" encoding="utf-8"?>
<items>
  <item>
    <Locality>locality1</Locality>
    <CollectorAndNumber>collectorAndNumber1</CollectorAndNumber>
    <Distribution>distribution1</Distribution>
    <Note>note1</Note>
  </item>
  <item>
    <Locality>locality2</Locality>
    <CollectorAndNumber>collectorAndNumber2</CollectorAndNumber>
    <Institution>institution2</Institution>
    <Distribution>distribution2</Distribution>
    <Note>note2</Note>
  </item>
  <item>
    <Locality>locality3</Locality>
    <CollectorAndNumber>collectorAndNumber3</CollectorAndNumber>
    <Institution>institution3</Institution>
    <Distribution>distribution3</Distribution>
  </item>
</items>

XSL transformation:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes"/>

  <xsl:template match="/">
    <summary>
      <xsl:apply-templates />
    </summary>
  </xsl:template>

  <xsl:template match="item">
    <item>
      <xsl:call-template name="join">
        <xsl:with-param name="list" select="Locality | CollectorAndNumber | Institution | Distribution | Note" />
        <xsl:with-param name="separator" select="','" />
      </xsl:call-template>
    </item>
  </xsl:template>

  <xsl:template name="join">
    <xsl:param name="list" />
    <xsl:param name="separator"/>

    <xsl:for-each select="$list">
      <xsl:value-of select="." />
      <xsl:if test="position() != last()">
        <xsl:value-of select="$separator" />
      </xsl:if>
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

Generated output document:

<?xml version="1.0" encoding="utf-8"?>
<summary>
  <item>locality1,collectorAndNumber1,distribution1,note1</item>
  <item>locality2,collectorAndNumber2,institution2,distribution2,note2</item>
  <item>locality3,collectorAndNumber3,institution3,distribution3</item>
</summary>

NB: If you were using XSLT/XPath 2.0 then there would be fn:string-join

fn:string-join**($operand1 as string*, $operand2 as string*) as string

which could be used as follows:

fn:string-join({Locality, CollectorAndNumber, Distribution, Note}, ",") 
Sailing answered 28/4, 2009 at 15:34 Comment(1)
Your answer is pretty much like Dimitre's. His is a little bit more elegant however. Thanks.Charissacharisse
V
3

Supposing you have something like the following input XML:

<root>
  <record>
    <Locality>Locality</Locality>
    <CollectorAndNumber>CollectorAndNumber</CollectorAndNumber>
    <Institution>Institution</Institution>
    <Distribution>Distribution</Distribution>
    <Note>Note</Note>
    <OtherStuff>Unimportant</OtherStuff>
  </record>
</root>

Then this template would do it:

<xsl:stylesheet
    version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>

  <xsl:output method="text" />

  <xsl:template match="record">
    <xsl:variable name="values">
      <xsl:apply-templates mode="concat" select="Locality" />
      <xsl:apply-templates mode="concat" select="CollectorAndNumber" />
      <xsl:apply-templates mode="concat" select="Institution" />
      <xsl:apply-templates mode="concat" select="Distribution" />
      <xsl:apply-templates mode="concat" select="Note" />
    </xsl:variable>

    <xsl:value-of select="substring($values, 1, string-length($values) - 1)" />
    <xsl:value-of select="'&#10;'" /><!-- LF -->
  </xsl:template>

  <xsl:template match="Locality | CollectorAndNumber | Institution | Distribution | Note" mode="concat">
    <xsl:value-of select="." />
    <xsl:text>,</xsl:text>
  </xsl:template>

</xsl:stylesheet>

Output on my system:

Locality,CollectorAndNumber,Institution,Distribution,Note
Venicevenin answered 28/4, 2009 at 15:18 Comment(1)
I edited my answer to provide a solution for arbitrary list and ordering of elements, as you suggested. Thanks!Bounds
S
2

I think it might be useful to mention, position() doesn't work right when I use a complicated select that filters some nodes, in that case I came up which this trick:

you can define a string variable that hold value of nodes, separated by a specific character, then by using str:tokenize() you can create a complete node list which position works fine with it.

something like this:

<!-- Since position() doesn't work as expected(returning node position of current 
node list), I got round it by a string variable and tokenizing it in which 
absolute position is equal to relative(context) position. -->
<xsl:variable name="measObjLdns" >
    <xsl:for-each select="h:measValue[@measObjLdn=$currentMeasObjLdn]/h:measResults" >
        <xsl:value-of select="concat(.,'---')"/> <!-- is an optional separator. -->
    </xsl:for-each>
</xsl:variable>

<xsl:for-each select="str:tokenize($measObjLdns,'---')" ><!-- Since position() doesn't 

work as expected(returning node position of current node list), 
I got round it by a string variable and tokenizing it in which
absolute position is equal to relative(context) position. -->

    <xsl:value-of select="."></xsl:value-of>
    <xsl:if test="position() != last()">
        <xsl:text>,</xsl:text>
    </xsl:if>
</xsl:for-each>
<xsl:if test="position() != last()">
    <xsl:text>,</xsl:text>
</xsl:if>
Subtotal answered 28/2, 2012 at 14:49 Comment(1)
Giving an explanation of your code might help reuse it. I have a similar problem with filtering of some of the nodes. But couldn't make out your answer esp. at the end of the day struggling with xslt's quirks. :-)Huppah
A
1

Do you not have a value that is always going to be there? If you do then you can turn it around and put commas infront of everything apart from the first item (which would be your value that's always there).

Alfonse answered 28/4, 2009 at 14:41 Comment(0)
C
-4

This would be a bit messy but might do the trick if there's only a few elements like in your example:

<xsl:if test="Locality != ''">
    <xsl:value-of select="Locality"/>
    <xsl:if test="CollectorAndNumber != '' or Institution != '' or Distribution != '' or Note != ''">
        <xsl:value-of select="','"/>
    </xsl:if>
</xsl:if>
<xsl:if test="CollectorAndNumber != ''">
    <xsl:value-of select="CollectorAndNumber"/>
    <xsl:if test="Institution != '' or Distribution != '' or Note != ''">
        <xsl:value-of select="','"/>
    </xsl:if>
</xsl:if>
<xsl:if test="Institution != ''">
    <xsl:value-of select="Institution"/>
    <xsl:if test="Distribution != '' or Note != ''">
        <xsl:value-of select="','"/>
    </xsl:if>
</xsl:if>
<xsl:if test="Distribution != ''">
    <xsl:value-of select="Distribution"/>
    <xsl:if test="Note != ''">
        <xsl:value-of select="','"/>
    </xsl:if>
</xsl:if>
<xsl:if test="Note != ''">
    <xsl:value-of select="Note"/>
</xsl:if>
Chartres answered 28/4, 2009 at 15:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.