Search code examples
xsltsumvalue-of

XSL, SUM & Multiply with Condition


Im doing an assignment for University (so im new to XSL coding) in making a quasi ecommerce site, and will provide as much detail as i can so it makes sense.

Sample XML Data:

<Items>
  <Item>
    <ItemID>50001</ItemID>
    <ItemName>Samsung Galaxy S4</ItemName>
    <ItemPrice>629</ItemPrice>
    <ItemQty>14</ItemQty>
    <ItemDesc>4G Mobile</ItemDesc>
    <QtyHold>0</QtyHold>
    <QtySold>1</QtySold>
  </Item>
  <Item>
    <ItemID>50002</ItemID>
    <ItemName>Samsung Galaxy S5</ItemName>
    <ItemPrice>779</ItemPrice>
    <ItemQty>21</ItemQty>
    <ItemDesc>4G Mobile</ItemDesc>
    <QtyHold>0</QtyHold>
    <QtySold>1</QtySold>
  </Item>
</Items>

Website

So the process is, when a person clicks 'Add to Cart' in the top Table, the ItemQty is decreased by 1 on the ItemQty in the XML, while it increases by 1 in the QtyHold in the XML. (QtyHold represents what has been added to the shopping Cart. Thus if QtyHold is >0 then its been added to the Cart)

My problem refers to the 2nd Table (code below), where the Total figure works - only if dealing with 1 Item. Thus, if Item Number '50001' is added a 2nd time, the Total wont change.

<xsl:template match="/">
    <fieldset>
    <legend>Shopping Cart</legend>
    <BR />
    <table border="1" id="CartTable" align="center">
    <tr><th>Item Number</th>
    <th>Price</th>
    <th>Quantity</th>
    <th>Remove</th></tr> 
    <xsl:for-each select="/Items/Item[QtyHold > 0]">
        <tr><td><xsl:value-of select="ItemID"/></td>
        <td>$<xsl:value-of select="ItemPrice"/></td>
        <td><xsl:value-of select="QtyHold"/></td>
        <td><button onclick="addtoCart({ItemID}, 'Remove')">Remove from Cart</button></td> </tr>
    </xsl:for-each>
    <tr><td ALIGN="center" COLSPAN="3">Total:</td><td>$<xsl:value-of select="sum(//Item[QtyHold >0]/ItemPrice)"/></td></tr>
    </table>
    <BR />
    <button onclick="Purchase()" class="submit_btn float_l">Confirm Purchase</button>
    <button onclick="CancelOrder()" class="submit_btn float_r">Cancel Order</button>
    </fieldset>
</xsl:template>
</xsl:stylesheet>

So what needs to happen is within the following code, while it checks if the QtyHold is greater than 0 (which would mean its in the shopping Cart) & to sum these values, it also needs to multiply QtyHold & ItemPrice.

<xsl:value-of select="sum(//Item[QtyHold >0]/ItemPrice)"/>

I tried many variations of Code like this below... but can't seem to make anything work.

select="sum(//Item[QtyHold >0]/ItemPrice)/(QtyHold*ItemPrice"/>

Solution

  • If you are using XSLT 2.0, the expression you could use would be this:

    <xsl:value-of select="sum(//Item[QtyHold >0]/(ItemPrice * QtyHold))"/>
    

    However, in XSLT 1.0 that is not allowed. Instead, you could achieve the result you need with an extension function. In particular the "node-set" function. First you would create a variable like this, in which you construct new nodes holding each item total

    <xsl:variable name="itemTotals">
         <xsl:for-each select="//Item[QtyHold >0]">
              <total>
                  <xsl:value-of select="ItemPrice * QtyHold" />
              </total>
         </xsl:for-each>
    </xsl:variable>
    

    Ideally, you would like to do sum($itemTotals/total), but this won't work, because itemTotals is a "Result Tree Fragment" and the sum function only accepts a node-set. So you use the node-set extension function to convert it. First declare this namespace in your XSLT...

     xmlns:exsl="http://exslt.org/common"
    

    Then, your sum function would look like this:

     <xsl:value-of select="sum(exsl:node-set($itemTotals)/total)"/>
    

    Alternatively, if you couldn't even use an extension function, you could use the "following-sibling" approach, to select each Item at a time, and keep a running total. So, you would have a template like this:

    <xsl:template match="Item" mode="sum">
        <xsl:param name="runningTotal" select="0" />
    
        <xsl:variable name="newTotal" select="$runningTotal + ItemPrice * QtyHold" />
        <xsl:variable name="nextItem" select="following-sibling::Item[1]" />
        <xsl:choose>
            <xsl:when test="$nextItem">
                <xsl:apply-templates select="$nextItem" mode="sum">
                    <xsl:with-param name="runningTotal" select="$newTotal" />
                </xsl:apply-templates>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$newTotal" />
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
    

    And to call it, to get the sum, you just start off by selecting the first node

    <xsl:apply-templates select="(//Item)[1]" mode="sum" />
    

    Try this XSLT which demonstrates the various approaches

    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
                   xmlns:exsl="http://exslt.org/common"
                   exclude-result-prefixes="exsl">
    
        <xsl:output method="html" indent="yes" />
    
        <xsl:template match="/">
            <table border="1" id="CartTable" align="center">
            <tr><th>Item Number</th>
            <th>Price</th>
            <th>Quantity</th>
            </tr> 
            <xsl:for-each select="/Items/Item[QtyHold > 0]">
                <tr>
                    <td><xsl:value-of select="ItemID"/></td>
                     <td>$<xsl:value-of select="ItemPrice"/></td>
                    <td><xsl:value-of select="QtyHold"/></td>
                </tr>
            </xsl:for-each>
            <tr>
                <td ALIGN="center" COLSPAN="2">Total:</td>
                <xsl:variable name="itemTotals">
                    <xsl:for-each select="//Item[QtyHold >0]">
                        <total>
                            <xsl:value-of select="ItemPrice * QtyHold" />
                        </total>
                    </xsl:for-each>
                </xsl:variable>
                <td>
                    <!-- XSLT 2.0 only: $<xsl:value-of select="sum(//Item[QtyHold >0]/(ItemPrice * QtyHold))"/>-->
                    $<xsl:value-of select="sum(exsl:node-set($itemTotals)/total)"/>
                    $<xsl:apply-templates select="(//Item)[1]" mode="sum" />
                </td>
            </tr>
            </table>
    </xsl:template>
    
    <xsl:template match="Item" mode="sum">
        <xsl:param name="runningTotal" select="0" />
    
        <xsl:variable name="newTotal" select="$runningTotal + ItemPrice * QtyHold" />
        <xsl:variable name="nextItem" select="following-sibling::Item[1]" />
        <xsl:choose>
            <xsl:when test="$nextItem">
                <xsl:apply-templates select="$nextItem" mode="sum">
                    <xsl:with-param name="runningTotal" select="$newTotal" />
                </xsl:apply-templates>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$newTotal" />
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
    </xsl:stylesheet>
    

    As a final thought, why don't you just a new Total element to each Item element in your XML. Initially, it would be set to 0, like QtyHold. Then, when you increment QtyHold by 1, by what ever process you do, you can also increment Total by the amount held in ItemPrice. That way, you can just sum this Total node to get the overall total, without the need for extension functions or recursive templates.