Search code examples
xmlxsltxslt-1.0xslt-grouping

Better way to merge node and sum attribute value based on another attribute value using single template in XSLT(1.0)


I need to get unique role with bar as child nodes where role/@totalQty is sum of all qty for that role. This has to be in 1 template using XSLT 1.0

I have written the following code. Is there a better way I can do it with lesser loops as I am looping 4 times , twice in for-loop ,once in sum & once in key ?

Input

<foo>
<bar  qty="1" unit="123">
<var  role="abc"/>
</bar>
<bar qty="1" unit="123">
<var  role="efg"/>
</bar>
<bar qty="2" unit="501">
<var  role="efg"/>
</bar>
<bar qty="1" unit="123">
<var  role="efg"/>
</bar>
<bar qty="1" unit="123">
<var  role="abc"/>
</bar>
<bar qty="1" unit="9085">
<var  role="abc"/>
</bar>
</foo>

Output

<foo>
<vars>
<var role="abc" totalQty="3">
<bar qty="1" unit="123" />
<bar qty="1" unit="123" />
<bar qty="1" unit="9085" />
</var>
<var role="efg" totalQty="4">
<bar qty="1" unit="123" />
<bar qty="2" unit="501" />
<bar qty="1" unit="123" />
</var>
</vars>
</foo>

xslt 1.0

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:key name="uniquerole" match="//bar/var" use="@role"/>
    <xsl:template match="/">
        <foo>
            <vars>
                <xsl:for-each select="//bar/var[generate-id() = generate-id(key('uniquerole', @role))]">
                    <xsl:variable name="vRole">
                        <xsl:value-of select="@role"/>
                    </xsl:variable>
                    <xsl:variable name="tot_qty">
                        <xsl:value-of select="sum(//bar[var/@role=$vRole]/@qty)"/>
                    </xsl:variable>
                    <var>
                        <xsl:attribute name="role">
                            <xsl:value-of select="$vRole"/>
                        </xsl:attribute>
                        <xsl:attribute name="totalQty">
                            <xsl:value-of select="$tot_qty"/>
                        </xsl:attribute>
                        <xsl:for-each select="//bar[var/@role=$vRole]">
                            <bar>
                                <xsl:attribute name="qty">
                                    <xsl:value-of select="@qty"/>
                                </xsl:attribute>
                                <xsl:attribute name="unit">
                                    <xsl:value-of select="@unit"/>
                                </xsl:attribute>
                            </bar>
                        </xsl:for-each>
                    </var>
                </xsl:for-each>
            </vars>
        </foo>
    </xsl:template>
</xsl:stylesheet>

Solution

  • Why not simply:

    XSLT 1.0

    <xsl:stylesheet version="1.0" 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
    
    <xsl:key name="bar-by-role" match="bar" use="var/@role"/>
    
    <xsl:template match="/foo">
        <foo>
            <vars>
                <xsl:for-each select="bar[generate-id() = generate-id(key('bar-by-role', var/@role)[1])]">
                    <xsl:variable name="grp" select="key('bar-by-role', var/@role)" />
                    <var role="{var/@role}" totalQty="{sum($grp/@qty)}">
                        <xsl:for-each select="$grp">
                            <bar qty="{@qty}" unit="{@unit}"/>
                        </xsl:for-each>
                    </var>
                </xsl:for-each>
            </vars>
        </foo>
    </xsl:template>
    
    </xsl:stylesheet>