How to select the smallest value from a bunch of variables?
Asked Answered
C

3

6

Assume I have variables $a, $b, $c and $d which all hold numbers. I would like to get the smallest (largest) value. My typical XSLT 1.0 approach to this is

<xsl:variable name="minimum">
  <xsl:for-each select="$a | $b | $c | $d">
    <xsl:sort
      select="."
      data-type="number"
      order="ascending" />
    <xsl:if test="position()=1"><xsl:value-of select="." /></xsl:if>
  </xsl:for-each>
</xsl:variable>

However, my xslt 1.0 processor complains with

runtime error: file stylesheet.xslt line 106 element for-each
The 'select' expression does not evaluate to a node set.

How can I compute the minimum (maximum) of the given values?


Of course, I could use a long series of <xsl:when> statements and check all combinations, but I'd rather like a smaller solution.

Ceaseless answered 27/4, 2012 at 16:45 Comment(2)
The main problem, i think, is that numbers are not nodes.Cheery
I think the main problem is that in XSLT 1.0, the expression in select must evaluate to a node-set. (w3.org/TR/xslt#for-each)Splotch
S
5

If the variables have statically defined values (not dynamically computed), then something like the following can be done with XSLT 1.0:

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

 <xsl:variable name="vA" select="3"/>
 <xsl:variable name="vB" select="1"/>
 <xsl:variable name="vC" select="9"/>
 <xsl:variable name="vD" select="5"/>

 <xsl:template match="/">
     <xsl:for-each select=
      "document('')/*/xsl:variable
         [contains('|vA|vB|vC|vD|', concat('|', @name, '|'))]
           /@select
      ">
      <xsl:sort data-type="number" order="ascending"/>

      <xsl:if test="position() = 1">
       Smallest: <xsl:value-of select="."/>
      </xsl:if>
      <xsl:if test="position() = last()">
       Largest: <xsl:value-of select="."/>
      </xsl:if>
     </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied on any XML document (not used), the wanted, correct result is produced:

   Smallest: 1
   Largest: 9

II. Now, suppose the variables are dynamically defined.

We can do something like this (but need the xxx:node-set() extension function):

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

 <xsl:variable name="vA" select="number(/*/*[3])"/>
 <xsl:variable name="vB" select="number(/*/*[1])"/>
 <xsl:variable name="vC" select="number(/*/*[9])"/>
 <xsl:variable name="vD" select="number(/*/*[5])"/>

 <xsl:template match="/">
     <xsl:variable name="vrtfStore">
       <num><xsl:value-of select="$vA"/></num>
       <num><xsl:value-of select="$vB"/></num>
       <num><xsl:value-of select="$vC"/></num>
       <num><xsl:value-of select="$vD"/></num>
     </xsl:variable>

     <xsl:for-each select="ext:node-set($vrtfStore)/*">
      <xsl:sort data-type="number" order="ascending"/>

      <xsl:if test="position() = 1">
       Smallest: <xsl:value-of select="."/>
      </xsl:if>
      <xsl:if test="position() = last()">
       Largest: <xsl:value-of select="."/>
      </xsl:if>
     </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the following XML document:

<nums>
  <num>01</num>
  <num>02</num>
  <num>03</num>
  <num>04</num>
  <num>05</num>
  <num>06</num>
  <num>07</num>
  <num>08</num>
  <num>09</num>
  <num>10</num>
</nums>

the wanted, correct result is produced:

   Smallest: 1
   Largest: 9
Submerged answered 28/4, 2012 at 1:47 Comment(3)
So the document('') points to the XSLT xml itself? Will it have to re-parse it? Another fancy trick I learner though, thanks. +1Powerful
@PavelVeller: Yes, this is often used if you want to have inline data in the XSLT stylesheet itself and not in a separate file. Yes, re-parse is done at execution time and this isn't too efficient. You may be interested to see the second part of the solution, too.Submerged
The second part does the trick, thanks. My variables are themselves minimums of more complicated expressions over the input tree, so certainly not static.Ceaseless
C
3

This XSLT 1.0 solution uses recursive templates to parse a delimited list of values to return the min/max value from the list.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

   <xsl:variable name="a" select="'3'"/>
    <xsl:variable name="b" select="'1'"/>
    <xsl:variable name="c" select="'9'"/>
    <xsl:variable name="d" select="'5'"/>

    <xsl:template match="/">
        <xsl:text>&#xa;Smallest: </xsl:text>
        <xsl:call-template name="min">
            <xsl:with-param name="values" select="concat($a,',',$b,',',$c,',',$d)"/>    
        </xsl:call-template>

        <xsl:text>&#xa;Largest: </xsl:text>
        <xsl:call-template name="max">
            <xsl:with-param name="values" select="concat($a,',',$b,',',$c,',',$d)"/>    
        </xsl:call-template>
    </xsl:template>

    <xsl:template name="min">
        <xsl:param name="values" />
        <xsl:param name="delimiter" select="','"/>
        <xsl:param name="min"/>

        <xsl:variable name="currentValue" >
            <xsl:choose>
                <xsl:when test="contains($values, $delimiter)">
                    <xsl:value-of select="substring-before($values,$delimiter)"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="$values"/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:variable>

        <xsl:variable name="minimumValue">
            <xsl:choose>
                <xsl:when test="$min and $min > $currentValue">
                    <xsl:value-of select="$currentValue"/>
                </xsl:when>
                <xsl:when test="$min">
                    <xsl:value-of select="$min"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="$currentValue" />
                </xsl:otherwise>
            </xsl:choose>
        </xsl:variable>

       <xsl:choose>
            <xsl:when test="substring-after($values,$delimiter)">
                <xsl:call-template name="min">
                    <xsl:with-param name="min" select="$minimumValue" />
                    <xsl:with-param name="values" select="substring-after($values,$delimiter)" />
                    <xsl:with-param name="delimiter" select="$delimiter"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$minimumValue" />
            </xsl:otherwise>
        </xsl:choose>                
    </xsl:template>


    <xsl:template name="max">
        <xsl:param name="values" />
        <xsl:param name="delimiter" select="','"/>
        <xsl:param name="max"/>

        <xsl:variable name="currentValue" >
            <xsl:choose>
                <xsl:when test="contains($values, $delimiter)">
                    <xsl:value-of select="substring-before($values,$delimiter)"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="$values"/>
                </xsl:otherwise>
            </xsl:choose>
        </xsl:variable>

        <xsl:variable name="maximumValue">
            <xsl:choose>
                <xsl:when test="$max and $currentValue > $max">
                    <xsl:value-of select="$currentValue"/>
                </xsl:when>
                <xsl:when test="$max">
                    <xsl:value-of select="$max"/>
                </xsl:when>
                <xsl:otherwise>
                    <xsl:value-of select="$currentValue" />
                </xsl:otherwise>
            </xsl:choose>
        </xsl:variable>

        <xsl:choose>
            <xsl:when test="substring-after($values,$delimiter)">
                <xsl:call-template name="max">
                    <xsl:with-param name="max" select="$maximumValue" />
                    <xsl:with-param name="values" select="substring-after($values,$delimiter)" />
                    <xsl:with-param name="delimiter" select="$delimiter"/>
                </xsl:call-template>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$maximumValue" />
            </xsl:otherwise>
        </xsl:choose>                
    </xsl:template>
</xsl:stylesheet>

When executed, produces the following output:

Smallest: 1
Largest: 9
Cyclostyle answered 28/4, 2012 at 2:36 Comment(0)
S
1

I've never had to do this in 1.0 (I use 2.0), but you could do this:

  <xsl:variable name="minimum">
    <xsl:choose>
      <xsl:when test="$b > $a and $c > $a and $d > $a"><xsl:value-of select="$a"/></xsl:when>
      <xsl:when test="$a > $b and $c > $b and $d > $b"><xsl:value-of select="$b"/></xsl:when>
      <xsl:when test="$b > $c and $a > $c and $d > $c"><xsl:value-of select="$c"/></xsl:when>
      <xsl:when test="$b > $d and $c > $d and $a > $d"><xsl:value-of select="$d"/></xsl:when>
    </xsl:choose>
  </xsl:variable>

There's got to be a better way though.

Splotch answered 27/4, 2012 at 17:37 Comment(2)
My thinking exactly. As I said, I already have this as a contingency plan. But it's not nice, and it certainly doesn't scale.Ceaseless
@Ceaseless - I totally missed the last paragraph of your question where you said you could do this. Hopefully someone (@DimitreNovatchev probably) will have a much better solution.Splotch

© 2022 - 2024 — McMap. All rights reserved.