codingManiac
codingManiac

Reputation: 1700

Summing calculated values in XSLT

I am trying to get the sum of all item prices multiplied by their quantity purchased, for example (given the following)...

<item>
   <itemPrice>10</itemPrice>
   <itemQty>5</itemQty> 
</item>
<item>
   <itemPrice>5</itemPrice>
   <itemQty>7</itemQty> 
</item>

I want to get a total value of $85. I have tried the following, but instead of adding each calculated item purchase to the total variable it concatenates all the values into a string...

<xsl:template name="itemsTotal">
    <xsl:variable name="total" select="0" />

    <xsl:for-each select="item">
        <xsl:value-of select="$total + (./itemQty * ./itemPrice)" />
    </xsl:for-each>
</xsl:template>

What's the best way to get what I am looking for? Is there a modification I can make to my template?

Upvotes: 0

Views: 194

Answers (2)

Tomalak
Tomalak

Reputation: 338386

Iteration is solved by recursion in XSLT 1.0.

<xsl:template name="itemTotal">
  <xsl:param name="item" select="." />
  <xsl:param name="carryOver" select="0" />

  <xsl:variable name="runningTotal" select="
    $carryOver + $item/itemPrice * $item/itemQty
  " />
  <xsl:variable name="nextItem" select="$item/following-sibling::item[1]" />

  <xsl:choose>
    <xsl:when test="not($nextItem)">
      <xsl:value-of select="$runningTotal" />
    </xsl:when>
    <xsl:otherwise>
      <xsl:call-template name="itemTotal">
        <xsl:with-param name="item" select="$nextItem" />
        <xsl:with-param name="carryOver" select="$runningTotal" />
      </xsl:call-template>
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

which can be called like this

<xsl:call-template name="itemTotal">
  <xsl:with-param name="item" select="root/item[1]" />
</xsl:call-template>

and outputs

85

Notes

  • This is XSLT 1.0. If your XSLT processor supports XSLT 2.0 and XQuery then you can solve this much more elegantly with an XQuery FLWOR expression.
  • The itemTotal template is tail-recursive, so it's likely to be optimized into a loop by the XSLT processor. In this case stack overflows cannot happen even for high <item> counts.
  • The template currently depends on the items being siblings (hence the use of following-sibling). This works right-away for your example but it might have to be adapted to other inputs.
  • No extension functions necessary, but if your XSLT processor supports them you might be better off using a node-set() based solution, like michael's answer shows.

Upvotes: 1

michael.hor257k
michael.hor257k

Reputation: 117140

Assuming you are using XSLT 1.0 and assuming that your processor supports the EXSLT node-set() function (which it almost certainly does) and assuming we add a root element to your XML input example and assuming you actually want the result to be 10*5 + 5*7 which is 85, not 80, try the following stylesheet:

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

<xsl:template match="/">
<!-- ... -->
<xsl:variable name="extPrices">
    <xsl:for-each select="items/item">
        <extPrice>
            <xsl:value-of select="itemPrice*itemQty" />
        </extPrice>
    </xsl:for-each>
</xsl:variable>
<xsl:variable name="subTotal" select="sum(exsl:node-set($extPrices)/extPrice)" />

<subTotal><xsl:value-of select="$subTotal" /></subTotal>
<!-- ... -->    
</xsl:template>
</xsl:stylesheet>

Applied to the corrected input:

<items>
    <item>
       <itemPrice>10</itemPrice>
       <itemQty>5</itemQty> 
    </item>
    <item>
       <itemPrice>5</itemPrice>
       <itemQty>7</itemQty> 
    </item>
</items>

the result is:

<?xml version="1.0" encoding="utf-8"?>
<subTotal>85</subTotal>

Upvotes: 1

Related Questions