tags:

views:

69

answers:

3
+3  Q: 

xsl transformation

Hi all,

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

+1  A: 

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
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
You are running this with XSLT1. Do you have an XSLT2 transformer available?
Jim Garrison
Which XSL processor are you using?
Jim Garrison
I changed the scheme to XSLT2.0 and got it to work :) thx
Rise_against
+2  A: 

I. An XSLT solution -- no extension functions required:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <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>

II. An XSLT 2.0 solution:

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <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:sequence select="sum(/*/Items/Item/(Price*Quantity))"/>
  </xsl:copy>
 </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the same XML document, the wanted, correct 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
@Dimitre: +1 for good XSLT 1.0 solution and XSLT 2.0 as well!
Alejandro
+1  A: 

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

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <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>
Alejandro
@Alejandro: Good answer (+1).
Dimitre Novatchev