XSLT merging/concatenating values of siblings nodes of same name into single node
Asked Answered
F

3

5

Input xml

<catalog>
    <product id="1">
        <name>abc</name>
        <category>aaa</category>
        <category>bbb</category>
        <category>ccc</category>
    </product>
    <product id="2">
        <name>cde</name>
        <category>aaa</category>
        <category>bbb</category>
    </product>
</catalog>

Expected Output xml

<products>
    <product>
        <id>1</id>
        <name>abc</name>
        <category>aaa,bbb,ccc</category>
    </product>
    <product>
        <id>2</id>
        <name>cde</name>
        <category>aaa,bbb</category>
    </product>
</products>

XSLT for transformation

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="/catalog">
        <products>
            <xsl:for-each select="product">
                <product>
                    <id><xsl:value-of select="@id"/></id>
                    <name><xsl:value-of select="name"/></name>
                    <category><xsl:value-of select="category" /></category>
                </product>
            </xsl:for-each>
        </products>
    </xsl:template>
</xsl:stylesheet>

Actual Output xml :(

<products>
    <product>
        <id>1</id>
        <name>abc</name>
        <category>aaa</category>
    </product>
    <product>
        <id>2</id>
        <name>cde</name>
        <category>aaa</category>
    </product>
</products>

Code needed in looping through all sibling node by the name 'category' under every 'product' and merging/concatenating into single node separated by a comma. Number of 'category' varies for every product and hence the count is unknown.

Firework answered 25/9, 2012 at 15:15 Comment(0)
B
9

Using this handy join call-template defined here, this becomes as simple as:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" indent="yes"/>
    <xsl:template match="/catalog">
        <products>
            <xsl:for-each select="product">
                <product>
                    <id>
                        <xsl:value-of select="@id"/>
                    </id>
                    <name>
                        <xsl:value-of select="name"/>
                    </name>
                    <category>
                        <xsl:call-template name="join">
                            <xsl:with-param name="list" select="category" />
                            <xsl:with-param name="separator" select="','" />
                        </xsl:call-template>
                    </category>
                </product>
            </xsl:for-each>
        </products>
    </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>

Output:

<products>
  <product>
    <id>1</id>
    <name>abc</name>
    <category>aaa,bbb,ccc</category>
  </product>
  <product>
    <id>2</id>
    <name>cde</name>
    <category>aaa,bbb</category>
  </product>
</products>
Buoyancy answered 25/9, 2012 at 15:25 Comment(0)
S
6

In XSLT 2.0 you only need to make one small change to your code:

<category><xsl:value-of select="category" separator=","/></category>

Note that if you require an XSLT 1.0 solution it's a good idea to say so. Some people in some environments are stuck on 1.0, but a lot of people aren't.

Schlemiel answered 25/9, 2012 at 16:38 Comment(2)
Ah yes, its 1.0, and the xslt stylesheet version says so.Firework
The version number on the stylesheet tells us nothing about the capabilities of the XSLT processor you are using, or the capability of the project to move to a more up-to-date processor.Schlemiel
O
3

Here's one other XSLT 1.0 solution.

When this XSLT:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
  <xsl:output omit-xml-declaration="no" indent="yes" />
  <xsl:strip-space elements="*" />

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="product">
    <xsl:copy>
      <xsl:apply-templates select="*[not(self::category)]" />
      <category>
        <xsl:apply-templates select="category/text()" />
      </category>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="category/text()">
    <xsl:if test="position() &gt; 1">,</xsl:if>
    <xsl:value-of select="."/>
  </xsl:template>
</xsl:stylesheet>

...is applied to the OP's original XML:

<catalog>
  <product id="1">
    <name>abc</name>
    <category>aaa</category>
    <category>bbb</category>
    <category>ccc</category>
  </product>
  <product id="2">
    <name>cde</name>
    <category>aaa</category>
    <category>bbb</category>
  </product>
</catalog>

...the desired result is produced:

<?xml version="1.0"?>
<catalog>
  <product>
    <name>abc</name>
    <category>aaa,bbb,ccc</category>
  </product>
  <product>
    <name>cde</name>
    <category>aaa,bbb</category>
  </product>
</catalog>

Explanation:

  • The first template -- the Identity Template -- matches all nodes and attributes and copies them to the result document as-is.
  • The second template overrides the Identity Template by creating a new <category> element and processing the text children of each <category> element in the current location of the document.
  • The final template outputs the text values and commas as necessary.
Oenone answered 25/9, 2012 at 22:4 Comment(1)
Haven't tried but appreciate the different solutions ABach :)Firework

© 2022 - 2024 — McMap. All rights reserved.