tags:

views:

2845

answers:

5

Hi everyone,

I am having a difficult time trying to do something that seems like it should be really easy to do. I basically want to multiply 2 numbers in a node and then sum the total of those numbers for all the nodes. Here is the XSLT code I have tried.

<xsl:value-of select="sum(Parts/Part/Quantity * Parts/Part/Rate)"/>

This code results in an error that says "Argument 1 of function sum cannot be converted to a node set."

Does anyone have an idea of what is wrong or how I can accomplish what I am trying to do?

Thanks

+2  A: 

There is a good example over here:

http://blog.davidkaspar.com/archives/2003/10/xslt-sum-of-products-from-multiple-nodes.php

Hopefully this helps

Deep Kapadia
@durple: The pointed-to solution uses Xalan's xalan:nodeset() extension function, which is absolutely unnecessary. Much better solutions do exist -- see my answer.
Dimitre Novatchev
+4  A: 

Here are three possible solutions:

Solution1 XSLT2:

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output method="text"/>

    <xsl:template match="/">
      <xsl:sequence select="sum(/*/*/(rate * quantity))"/>
    </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the following XML document:

<parts>
  <part>
     <rate>0.37</rate>
    <quantity>10</quantity>
  </part>
  <part>
     <rate>0.03</rate>
    <quantity>10</quantity>
  </part>
</parts>

The wanted result is produced:

4

The XSLT 2.0 solution uses the fact that in XPath 2.0 it is allowed that the right argument of the last "/" operator can be an expression or generally a function. This expression/function is applied for each of the nodes selected so far acting as the context node, and each function application produces one result.

Solution2 XSLT 1.0:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output method="text"/>

    <xsl:template match="/">
      <xsl:call-template name="sumProducts">
        <xsl:with-param name="pList" select="*/*"/>
      </xsl:call-template>
    </xsl:template>

    <xsl:template name="sumProducts">
        <xsl:param name="pList"/>
        <xsl:param name="pAccum" select="0"/>

        <xsl:choose>
          <xsl:when test="$pList">
            <xsl:variable name="vHead" select="$pList[1]"/>

            <xsl:call-template name="sumProducts">
              <xsl:with-param name="pList" select="$pList[position() > 1]"/>
              <xsl:with-param name="pAccum"
               select="$pAccum + $vHead/rate * $vHead/quantity"/>
            </xsl:call-template>
          </xsl:when>
          <xsl:otherwise>
            <xsl:value-of select="$pAccum"/>
          </xsl:otherwise>
        </xsl:choose>
    </xsl:template>
</xsl:stylesheet>

When applied on the above XML document, the correct result is produced:

4

This is a typical XSLT 1.0 recursive solution. Do note how the sumProducts template calls itself recursively, until the entire input list, passed in the parameter $pList is processed.

Solution3 FXSL (XSLT 1.0):

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common"
xmlns:test-map-product="test-map-product"
exclude-result-prefixes="xsl ext test-map-product"
>
   <xsl:import href="sum.xsl"/>
   <xsl:import href="map.xsl"/>
   <xsl:import href="product.xsl"/>

   <!-- This transformation is to be applied on:
        salesMap.xml

        It contains the code of the "sum of products" from the 
        article "The Functional Programming Language XSLT"
     -->

   <test-map-product:test-map-product/>

   <xsl:output method="text"/>

   <xsl:template match="/">
     <!-- Get: map product /sales/sale -->
     <xsl:variable name="vSalesTotals">
         <xsl:variable name="vTestMap" select="document('')/*/test-map-product:*[1]"/>
         <xsl:call-template name="map">
           <xsl:with-param name="pFun" select="$vTestMap"/>
           <xsl:with-param name="pList1" select="/sales/sale"/>
         </xsl:call-template>
     </xsl:variable>

     <!-- Get sum map product /sales/sale -->
      <xsl:call-template name="sum">
        <xsl:with-param name="pList" select="ext:node-set($vSalesTotals)/*"/>
      </xsl:call-template>
   </xsl:template>

    <xsl:template name="makeproduct" match="*[namespace-uri() = 'test-map-product']">
      <xsl:param name="arg1"/>

      <xsl:call-template name="product">
        <xsl:with-param name="pList" select="$arg1/*"/>
      </xsl:call-template>
    </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the following XML document:

<sales>
  <sale>
    <price>3.5</price>
    <quantity>2</quantity>
    <Discount>0.75</Discount>
    <Discount>0.80</Discount>
    <Discount>0.90</Discount>
  </sale>
  <sale>
    <price>3.5</price>
    <quantity>2</quantity>
    <Discount>0.75</Discount>
    <Discount>0.80</Discount>
    <Discount>0.90</Discount>
  </sale>
</sales>

The correct result is produced:

7.5600000000000005

In the last case for each sale we calculate the product of price, quantity and all available (variable number of) discount-s.

FXSL is a pure XSLT implementation of higher order functions. In this example the higher-order function f:map() is used to map the function f:product() on the list of children of every sale element. Then the results are summed to produce the final result.

Dimitre Novatchev
Thanks. Solution #2 was exactly what I needed to create an aggregate multiplication function.
Darrel Miller
+1 for effort and spending time .. :-)
infant programmer
@infant-programmer: Thanks for your appreciation! If you liked this you're going to like a lot of the stuff on my blog: http://dnovatchev.spaces.live.com/Blog/
Dimitre Novatchev
A: 

Hi Guys,

Thanks for the responses. The one I am trying to use keeps giving me an error.

<xsl:template match="/">
  <xsl:sequence select="sum(/*/*/(rate * quantity))"/>
</xsl:template>

"Xsl:sequence cannot be a child of the 'xsl:template' element. I get this error just about anywhere I try using sxl:sequence.

Any ideas of why?

Thanks

@Dave: Are you sure you are using an XSLT 2.0 processor?
Dimitre Novatchev
Hi Dimitre, I am not sure if I am using an XSLT 2.0 processor or not. How do I tell? Below is what I have in my Stylesheet.<?xml version="1.0" encoding="utf-16" ?><xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt">
A: 

Hi,

All of Dimitre's solutions work and he's right that you don't need to use extension functions but sometimes it makes life easier. It's not too harmful, especially when you use exslt extensions which are supported across multiple XSLT processors. Also, the reason you're getting the sequence errors is probably because you're using an XSLT 1 processor.

If you want to persist with your chosen solution, you'll need to use Saxon or some other XSLT processor that supports XSLT 2.

Otherwise, here's an alternative method of doing it in XSLT 1. This will work in most XSLT processors and some peope might find it easier to grok than the recursive version. Personally, I prefer the recursive version (Dimitre's 3rd proposal) because it is more portable.

<xsl:stylesheet version="1.0"
                xmlns:ex="http://exslt.org/common"
       xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
  <xsl:output method="text"/>

  <xsl:template name="GetProducts">
    <xsl:param name="left"/>
    <xsl:param name="right"/>

    <xsl:for-each select="$left/text()">
      <product>
     <xsl:value-of select="number(.) * number($right[position()])"/>
      </product>
    </xsl:for-each>
  </xsl:template>

  <xsl:template match="/">
    <xsl:variable name="products">
      <xsl:call-template name="GetProducts">
     <xsl:with-param name="left" select="Parts/Part/Rate"/>
     <xsl:with-param name="right" select="Parts/Part/Quantity"/>
      </xsl:call-template>
    </xsl:variable>

    <xsl:value-of select="sum(ex:node-set($products)/product)"/>
  </xsl:template>
</xsl:stylesheet>
cddr