2

I have a fairly convoluted XML file and I need to do a weighted average of a few values within it using XSL. I am able to complete a sum of the weights OR of the values, but I cannot get the multiplication to work. I get an error:

XPTY0004: A sequence of more than one item is not allowed as the first operand of '*'

I am not able to share the XML, but I have simplified the XML to the following example (assume there are a large number of foos):

<group>
<fooList>
    <foo>
        <attributeList>
            <Attribute ID="1" Weight="0.5">
                <otherParams />
            </Attribute>
        </attributeList>
        <Properties>
            <PhysicalProperties>
                <Volume Average="125" Unknown="50" />
            </PhysicalProperties>
        </Properties>
     </foo>
</fooList>
</group>

My current attempt to get the weighted average is the following:

<xsl:variable name="WeightedVolume" select="sum(/group/fooList/foo[attributeList/Attribute/[@ID=$test_id]]/attributeList/Attribute/@Weight * /group/fooList/foo[attributeList/Attribute/[@ID=$test_id]]/Properties/PhysicalProperties/Volume/@Average)"/>

I know there are similar questions available - but most of them deal with something like summing and multiplying foo

<foo>
    <Weight>0.5</Weight>
    <VolumeAverage>125</VolumeAverage>
</foo>

The answer on this StackOverflow Question appeals to me, but I cannot seem to make it work.

I'm using Saxon-HE 9.5.1.1N from Saxonica, with Visual Studio 2013.

Edited I was able to get something to work for XSL 2, but need to have a fall-back for XSL1.

<xsl:variable name="WeightedVolume" select="sum(for $i in /group/FooList/foo[attributeList/Attribute[@ID=$test_id] return $i/AttributeList/Attribute/@Weight * $i/Properties/PhysicalProperties/Volume/@Average)"/>
Community
  • 1
  • 1
Kevin K
  • 408
  • 2
  • 18
  • Could you provide us with some valid XML? Is `Properties` a child of `Attribute`, `otherParams`, `foo`, or something else? Your attempted path suggests that it is a child of `Attribute`, but your XML example suggests that it is a child of "who knows". – JLRishe Jan 14 '15 at 19:52
  • @kevin, with XSLT 1.0 you can't do it with pure XPath, you have to write a recursive templates with parameters. – Martin Honnen Jan 14 '15 at 20:23
  • Good examples how this is done can be found here: http://stackoverflow.com/questions/436998/multiply-2-numbers-and-then-sum – matthias_h Jan 14 '15 at 20:30
  • @matthias_h OP already linked to that post in his question. :) – JLRishe Jan 14 '15 at 20:44
  • @JLRishe Thanks for the info and sorry to Kevin, just overread that. – matthias_h Jan 14 '15 at 20:52
  • No worries matthias_h. Your recommendation more or less validated that page in my eyes! – Kevin K Jan 14 '15 at 21:25

2 Answers2

3

To follow the example in that question you linked to, you would use this in XSLT 2.0/XPath 2.0:

<xsl:variable name="FoosToCalculate"
              select="/group/fooList/foo[attributeList/Attribute/@ID = $test_id]" />
<xsl:variable name="WeightedVolume" 
              select="sum($FoosToCalculate/(attributeList/Attribute/@Weight * 
                                 Properties/PhysicalProperties/Volume/@Average)
                          )"/>

Doing this summing in XSLT 1.0 is considerably more involved and typically involves either using recursive templates or some manifestation of the node-set() function. Here is an example of the latter:

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

  <xsl:template match="/">
    <!-- determine $test_id however you need to -->
    <xsl:variable name="products">
      <xsl:for-each 
           select="/group/fooList/foo[attributeList/Attribute/@ID = $test_id]">
        <product>
          <xsl:value-of select="attributeList/Attribute/@Weight * 
                                Properties/PhysicalProperties/Volume/@Average" />
        </product>
      </xsl:for-each>
    </xsl:variable>

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

</xsl:stylesheet>
JLRishe
  • 99,490
  • 19
  • 131
  • 169
  • JLRishe - I will go with this answer since it addresses both issues at hand. I was able to make it work on the demo XML, and now have to massage it into the real version. Thank you everyone! – Kevin K Jan 14 '15 at 21:24
  • I had it working mostly, but I'm having some issues with the line of your example. Is that supposed to be ? – Kevin K Jan 14 '15 at 22:48
  • @KevinKennedy Sorry, that should have been `` and I noticed one more mistake. Please see my edit. – JLRishe Jan 14 '15 at 22:50
1

For completeness, if you want to sum over a computed quantity in XSLT 1.0, there are three ways of doing it:

(a) recursion: write a recursive template that processes the items in the sequence one by one, computing the total as it goes.

(b) create an XML tree in which the computed quantities are node values, and then process this tree using the sum() function. To do this in a single stylesheet you will need the exslt:node-set() extension function.

(c) use an extension function provided by the XSLT vendor, or user-written using the facilities provided by the vendor for calling external functions.

In XSLT 2.0, it can always be done using the construct

sum(for $x in node-set return f($x))

where f is a function that computes the quantity.

Michael Kay
  • 156,231
  • 11
  • 92
  • 164