5

I am new to xsl transformations and I have a question. I am looping through an xml like this:

 <PO>
<Items>
  <Item>
     <Price>2</Price>
     <Quantity>5</Quantity>
  </Item>
  <Item>
     <Price>3</Price>
     <Quantity>2</Quantity>
  </Item>    
 </Items>
 <QuantityTotal></QuantityTotal>
 </PO>

Now I want to insert a value in the QuantityTotal node:
The value is the sum of price*quantity of all items, in this case (2*5)+(3*2) = 16 How can I do this, I tried it with a loop and variables, but variables are immutable so I don't know how I can achieve this.

Thx for your help

Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
Rise_against
  • 1,042
  • 3
  • 15
  • 36
  • Good question (+1). See my answer for solutions in both XSLT 1.0 (no extensions required), and XSLT 2.0 – Dimitre Novatchev Aug 27 '10 at 13:15
  • See also http://stackoverflow.com/questions/436998/multiply-2-numbers-and-then-sum-with-xslt and http://stackoverflow.com/questions/1333558/xslt-to-sum-product-of-two-attributes – harpo Aug 27 '10 at 20:22

3 Answers3

4

Here is an XSLT solution -- no extension functions required:

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

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

 <xsl:template match="QuantityTotal">
  <xsl:copy>
   <xsl:call-template name="sumProducts">
     <xsl:with-param name="pNodes" select="../Items/Item"/>
   </xsl:call-template>
  </xsl:copy>
 </xsl:template>

 <xsl:template name="sumProducts">
  <xsl:param name="pNodes"/>
  <xsl:param name="pSum" select="0"/>
  <xsl:param name="pEname1" select="'Price'"/>
  <xsl:param name="pEname2" select="'Quantity'"/>

  <xsl:choose>
   <xsl:when test="not($pNodes)">
    <xsl:value-of select="$pSum"/>
   </xsl:when>
  <xsl:otherwise>
    <xsl:call-template name="sumProducts">
      <xsl:with-param name="pNodes" select=
      "$pNodes[position() > 1]"/>
      <xsl:with-param name="pSum" select=
      "$pSum
      +
       $pNodes[1]/*[name()=$pEname1]
      *
       $pNodes[1]/*[name()=$pEname2]
       "/>
    </xsl:call-template>
  </xsl:otherwise>
  </xsl:choose>
 </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the provided XML document:

<PO>
    <Items>
        <Item>
            <Price>2</Price>
            <Quantity>5</Quantity>
        </Item>
        <Item>
            <Price>3</Price>
            <Quantity>2</Quantity>
        </Item>
    </Items>
    <QuantityTotal></QuantityTotal>
</PO>

the wanted result is produced:

<PO>
   <Items>
      <Item>
         <Price>2</Price>
         <Quantity>5</Quantity>
      </Item>
      <Item>
         <Price>3</Price>
         <Quantity>2</Quantity>
      </Item>
   </Items>
   <QuantityTotal>16</QuantityTotal>
</PO>
Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
2

Besides Dimitre's excellent answer, this stylesheet takes other approach:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:template match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="QuantityTotal">
        <xsl:copy>
            <xsl:apply-templates select="../Items/Item[1]" mode="sum"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="Item" mode="sum">
        <xsl:param name="pSum" select="0"/>
        <xsl:variable name="vNext" select="following-sibling::Item[1]"/>
        <xsl:variable name="vSum" select="$pSum + Price * Quantity"/>
        <xsl:apply-templates select="$vNext" mode="sum">
            <xsl:with-param name="pSum" select="$vSum"/>
        </xsl:apply-templates>
        <xsl:if test="not($vNext)">
            <xsl:value-of select="$vSum"/>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

Output:

<PO>
    <Items>
        <Item>
            <Price>2</Price>
            <Quantity>5</Quantity>
        </Item>
        <Item>
            <Price>3</Price>
            <Quantity>2</Quantity>
        </Item>
    </Items>
    <QuantityTotal>16</QuantityTotal>
</PO>
1

Here's a solution using XSLT2, in which node-sets are first-class objects. In XSLT1 you'd need to use a node-set extension.

Explanation below:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.0">

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

    <xsl:variable name="extendedItems" as="xs:integer*">
        <xsl:for-each select="//Item">
             <xsl:value-of select="./Price * ./Quantity"/>
        </xsl:for-each>
    </xsl:variable>

    <xsl:variable name="total">
        <xsl:value-of select="sum($extendedItems)"/>
    </xsl:variable>

    <xsl:template match="//QuantityTotal">
        <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:value-of select="$total"/>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

The approach here is to use an "Identity transform" to copy the document, while performing the calculations and inserting the result into the output QuantityTotal template. The first template copies the input to the output but is overridden by a more-specific template for QuantityTotal at the bottom. The first variable declaration creates a list of extended costs, and the second variable definition sums the costs to produce the total. The total is then inserted into the QuantityTotal node.

The key to understanding XSL is that it is declarative in nature. The most common conceptual error made by almost all beginners is to assume that the stylesheet is a sequential program that processes the input XML document. In reality it's the other way around. The XSL engine reads the XML document. and for each new tag it encounters it looks in the stylesheet for the "best" match, executing that template.

EDIT:

Here's an xslt1.1 version that works with Saxon 6.5

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:ex="http://exslt.org/common"
    extension-element-prefixes="ex"
    version="1.1">
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:variable name="extendedItems">
        <xsl:for-each select="//Item">
            <extended>
             <xsl:value-of select="./Price * ./Quantity"/>
            </extended>
            </xsl:for-each>
    </xsl:variable>
    <xsl:variable name="total">
        <xsl:value-of select="sum(ex:node-set($extendedItems/extended))"/>
    </xsl:variable>
    <xsl:template match="//QuantityTotal">
        <xsl:copy>
            <xsl:apply-templates select="@*"/>
            <xsl:value-of select="$total"/>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>
Jim Garrison
  • 85,615
  • 20
  • 155
  • 190
  • thank you for your reply, but it still doesn't work like it is supposed to. The result I get now is 106, what is does is it calculates the first item (result=10) and the second item (result=6) and so the variable extendedItems it's value becomes "106". Also when I use the sum() function for the total, it throws an error that I have to use a node-set() so my code for total becomes like this: sum(msxsl:node-set($extendedItems)). What am I doing wrong? Thx in advance – Rise_against Aug 27 '10 at 09:05
  • You are running this with XSLT1. Do you have an XSLT2 transformer available? – Jim Garrison Aug 27 '10 at 12:41
  • Which XSL processor are you using? – Jim Garrison Aug 27 '10 at 12:55