tags:

views:

290

answers:

5

Using xslt/xpath, I need to be able to select the two elements which have the lowest attribute value and merge them, in a way. Lets say for example I have:

<root>
 <integer val="14"/>
 <integer val="7"/>
 <integer val="2"/>
 <integer val="1"/>
 <integer val="4"/> 
 <integer val="8"/>
</root>

I want to select the two lowest values (1 and 2) and represent them as one element in the output. The attribute value should be the sum of these two lowest values, so I want:

<root>
 <integer val="3"/>
</root>

I am also constrained to using only xslt 1.0, as the xml is to be processed with the java 1.5 api, which does not seem to support xslt 2.0. What should i do to make my stylesheet solve this seemingly simple task?


My first attempt was to use sorting:

<xsl:template match="root">
<xsl:copy>
 <xsl:apply-templates select="integer">
  <xsl:sort data-type="number" select="@val"/>  
 </xsl:apply-templates>
</xsl:copy>
</xsl:template>



<xsl:template match="integer[1]"> 
<xsl:copy>
 <xsl:attribute name="val">
  <xsl:value-of select="@val + ../integer[2]/@val"/>
 </xsl:attribute>
</xsl:copy>
</xsl:template>

This however results in nothing. Only an empty root node. Apparently, the <xsl:sort> disables the ability to do <xsl:template match="integer[1]"> (the [1] part is the part that does not work together with the sort). And, even if it did work, the [1] seems to refer to the document order, not the sorted order. Changing the second template to:

<xsl:template match="integer"> 
<xsl:copy>
 <xsl:attribute name="val">
  <xsl:value-of select="../integer[2]/@val"/>
 </xsl:attribute>
</xsl:copy>
</xsl:template>

Results in output where all output val attributes are 7 (instead of 2, which I wanted it to be)

Another approach was to use the min() xpath function. This however, failed fast since min() is not available in 1.0. And, even if min were available, it would not be trivial to find the two smallest elements and merge those.

A: 

Here is a "Starter for 10", its ugly but it works:-

<xsl:template match="root">
  <xsl:copy>
 <xsl:for-each select="integer">
   <xsl:sort data-type="number" select="@val"/>
   <xsl:if test="position() = 1">
  <xsl:variable name="lowest" select="." />
  <xsl:for-each select="../integer[count(. | $lowest) &gt; 1]">
    <xsl:sort data-type="number" select="@val"/>
    <xsl:if test="position() = 1">
   <integer val="{number(@val) + number($lowest/@val)}" />
    </xsl:if>
  </xsl:for-each>
   </xsl:if>
 </xsl:for-each>
  </xsl:copy>
</xsl:template>
AnthonyWJones
+1  A: 

Not the nicest solution, but it works:

<xsl:template match="/">
<xsl:variable name="first">
<xsl:call-template name="getNum">
    <xsl:with-param name="pos" select="1"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="second">
<xsl:call-template name="getNum">
        <xsl:with-param name="pos" select="2"/>
</xsl:call-template>
</xsl:variable>

<p><xsl:value-of select="$first + $second"/></p>
</xsl:template>

<xsl:template name="getNum">
    <xsl:param name="pos"/>
    <xsl:for-each select="/root/integer">
        <xsl:sort data-type="number" select="@val" order="ascending"/>
        <xsl:if test="position()=$pos">
                <xsl:value-of select="@val"/>
        </xsl:if>
    </xsl:for-each>

</xsl:template>
Niko
+2  A: 

My proposal (note that this does not create the sum of the distinct values):

<xsl:template match="root">
  <xsl:copy>
    <xsl:variable name="limit" select="2" />

    <!-- construct a comma-separated list of relevant IDs -->
    <xsl:variable name="idlist">
      <xsl:value-of select="','" />
      <xsl:for-each select="integer">
        <xsl:sort select="@val" data-type="number" order="ascending" />
        <xsl:if test="position() &lt;= $limit">
          <xsl:value-of select="generate-id()" />
          <xsl:value-of select="','" />
        </xsl:if>
      </xsl:for-each>
    </xsl:variable>

    <!-- sum up all nodes that are contained in the list -->
    <integer>
      <xsl:value-of select="
        sum(
          integer[
            contains(
              $idlist,
              concat(',', generate-id(), ',')
            )
          ]/@val
        )
      " />
    </integer>
  </xsl:copy>
</xsl:template>

This is not exactly elegant, maybe there's a nicer way to do this. My goal was to keep hard-coded sections minimal.

To create a distinct sum some kind of grouping needs to be applied in the for-each. Thinking about it, this question looks a little bit like a homework assignment to me.

Tomalak
A: 

Similar to Tomalak's solution, if you were able to utilise extension functions in your XSLT (they are supported in many XSLT 1.0 processors), you could create a node-set of a sorted list of intger elements, and then sum the first two values

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
    xmlns:exsl="urn:schemas-microsoft-com:xslt" 
    version="1.0" 
    extension-element-prefixes="exsl">

    <xsl:output method="xml" omit-xml-declaration="yes"/>

    <xsl:template match="/root">
     <root>
      <xsl:variable name="limit" select="2"/>
      <xsl:variable name="sortedlist">
       <xsl:for-each select="integer">
        <xsl:sort select="@val" data-type="number" order="ascending"/>
         <xsl:copy>
          <xsl:copy-of select="@*" />
         </xsl:copy>
       </xsl:for-each>
      </xsl:variable>
      <integer>
       <xsl:attribute name="val">
        <xsl:value-of select="sum(exsl:node-set($sortedlist)/integer[position() &lt;= $limit]/@val)"/>
       </xsl:attribute>
      </integer>
     </root>
    </xsl:template>
</xsl:stylesheet>
Tim C
A: 

This does the trick, with one caveat:

<xsl:template match="root">
  <xsl:variable name="integers">
    <xsl:for-each select="integer">
      <xsl:sort select="@val" data-type="number"/>
      <xsl:if test="position() &lt;= 2">
        <xsl:copy-of select="."/>
      </xsl:if>
    </xsl:for-each>
  </xsl:variable>
  <root>
    <integer val="{sum(msxsl:node-set($integers)/integer/@val)}"/>
  </root>
</xsl:template>

The caveat is that using for-each or apply-templates inside a variable returns a result tree fragment. Unless you're going to copy it to the output, there's next to nothing you can do with a result tree fragment; to use it in an XPath expression, you have to convert it to a node set.

That conversion requires an extension function, which may or may not be available in Java (this example uses Microsoft's XSLT processor).

Robert Rossney
`node-set()` is semi-standardized as part of EXSLT. While MSXSL doesn't support it, virtually everything else does (including `XslCompiledTransform`).
Pavel Minaev