Pete
Pete

Reputation: 3

How to get the grand total in an aggregate sum in XSLT?

First, I want to thank you for your time to help. I'm new to XSLT and am stuck for days trying to get the grand total of quantity. I am able to get the aggregate sum for each report_id by type and discard the negative aggregate sum (see report_id 222 and 444).

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <report>
        <report_id>111</report_id>
        <group>
            <type>A</type>
            <quantity>2</quantity>
        </group>
        <group>
            <type>B</type>
            <quantity>4</quantity>
        </group>
        <group>
            <type>A</type>
            <quantity>6</quantity>
        </group>
    </report>
    <report>
        <report_id>222</report_id>
        <group>
            <type>A</type>
            <quantity>-5</quantity>
        </group>
        <group>
            <type>A</type>
            <quantity>2</quantity>
        </group>
    </report>
    <report>
        <report_id>333</report_id>
        <group>
            <type>B</type>
            <quantity>7</quantity>
        </group>
    </report>
    
    <report>
        <report_id>444</report_id>
        <group>
            <type>B</type>
            <quantity>-12</quantity>
        </group>
    </report>
</root>

My xsl output is below, and the problem lies at the last value 0. 111 A=8; 111 B=4; 333 B=7; 0

The output should be this 111 A=8; 111 B=4; 333 B=7; 19

I tried call-template to do the $grand_qty and failed. I prefer XSLT 2.0 but version is find too. Thanks in advance.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet exclude-result-prefixes="xsl"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
    <xsl:output method="text" />
    <xsl:template match="/root">
        <xsl:variable name="total_qty" select="0"/>
        <xsl:variable name="grand_qty" select="0"/>
        <xsl:for-each-group select="report/group" group-by="concat(../report_id, '|', type)">
            <xsl:variable name="qty" select="sum(current-group()/quantity)"/>
            <xsl:if test="$qty &gt; 0">
                <xsl:variable name="total_qty" select="$qty + $total_qty"/>
                <xsl:value-of select="concat(../report_id, ' ', type, '=', $total_qty, '; ')"/>
                <xsl:variable name="grand_qty" select="$grand_qty + $total_qty"/>
            </xsl:if>
        </xsl:for-each-group>
        <xsl:value-of select="$grand_qty"/>
    </xsl:template>
</xsl:stylesheet>

Upvotes: 0

Views: 128

Answers (2)

Martin Honnen
Martin Honnen

Reputation: 167706

As your code uses version="3.0" I would guess XSLT 3 also works; there you have some ways to store values with maps or arrays, xsl:iterate or fold-left; together with grouping it is possible but a bit convoluted to keep a total parameter as follows:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="3.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="#all"
  xmlns:mf="http://example.com/mf"
  expand-text="yes">
  
  <xsl:function name="mf:group" as="map(*)*">
    <xsl:param name="group-elements" as="element(group)*"/>
    <xsl:for-each-group select="$group-elements" composite="yes" group-by="../report_id, type">
       <xsl:variable name="qty" select="sum(current-group()/quantity)"/>
       <xsl:sequence select="map { 'qty' : $qty, 'value' : current-grouping-key()[1] || ' ' || current-grouping-key()[2] || ' = ' || $qty }[$qty gt 0]"/>
    </xsl:for-each-group>
  </xsl:function>
  
  <xsl:output method="text" item-separator="; "/>

  <xsl:template match="/" name="xsl:initial-template">
     <xsl:iterate select="root/mf:group(report/group)">
       <xsl:param name="total" select="0"/>
       <xsl:on-completion>
         <xsl:sequence select="$total"/>
       </xsl:on-completion>
       <xsl:sequence select="?value"/>
       <xsl:next-iteration>
         <xsl:with-param name="total" select="?qty + $total"/>
       </xsl:next-iteration>
     </xsl:iterate>
  </xsl:template>
  
</xsl:stylesheet>

Alternative with fold-left instead of xsl:iterate:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="3.0"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="#all"
  xmlns:mf="http://example.com/mf"
  expand-text="yes">
  
  <xsl:function name="mf:group" as="map(*)*">
    <xsl:param name="group-elements" as="element(group)*"/>
    <xsl:for-each-group select="$group-elements" composite="yes" group-by="../report_id, type">
       <xsl:variable name="qty" select="sum(current-group()/quantity)"/>
       <xsl:sequence select="map { 'qty' : $qty, 'value' : current-grouping-key()[1] || ' ' || current-grouping-key()[2] || ' = ' || $qty }[$qty gt 0]"/>
    </xsl:for-each-group>
  </xsl:function>
  
  <xsl:output method="text" item-separator="; "/>

  <xsl:template match="/" name="xsl:initial-template">
     <xsl:sequence
       select="fold-left(
                root!mf:group(report!group),
                0,
                function($a, $g) { $a[position() lt last()], $g?value, $a[last()] + $g?qty  }
              )"/>
  </xsl:template>
  
</xsl:stylesheet>

Upvotes: 0

michael.hor257k
michael.hor257k

Reputation: 117073

Variables in XSLT are immutable, and limited in scope to their parent element (or more precisely, to all following siblings and their descendants).

Try a different approach:

XSLT 2.0

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

<xsl:template match="/root">
    <xsl:variable name="groups">
        <xsl:for-each-group select="report/group" group-by="concat(../report_id, ' ', type)">
            <xsl:variable name="qty" select="sum(current-group()/quantity)"/>
            <xsl:if test="$qty gt 0">
                <group name="{current-grouping-key()}">
                    <xsl:value-of select="$qty"/>
                </group>    
            </xsl:if>
        </xsl:for-each-group>
    </xsl:variable>
    <xsl:value-of select="$groups/group/concat(@name, '=', ., '; ')" separator=""/>
    <xsl:value-of select="sum($groups/group)"/>
</xsl:template>

</xsl:stylesheet>

Upvotes: 1

Related Questions