3

Greetings, given a list of numbers, is it possible to find a value closest to a given value with xslt? For example, if I'm looking for a value closest to 5 in a list [1,7] then 7 would be it. Likewise for [4,9], it would be 4. The list can be any length.

Thanks.

Matt
  • 45
  • 1
  • 5
  • I'm about to duck out, but check out http://www.xml.com/pub/a/2001/05/07/xsltmath.html and http://stackoverflow.com/questions/445782/finding-closest-match-in-collection-of-numbers – Aaron Newton Jan 31 '11 at 23:32
  • Good question, +1. See my answer for two solutions: XSLT 1.0 and XSLT 2.0. :) – Dimitre Novatchev Feb 01 '11 at 03:05

2 Answers2

2

This stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:param name="pValue" select="5"/>
    <xsl:template match="/">
        <xsl:for-each select="list/num">
            <xsl:sort select="(. - $pValue) * not(0 > . - $pValue )
                              - (. - $pValue) * (0 > . - $pValue)"/>
            <xsl:if test="position() = 1">
                <xsl:value-of select="."/>
            </xsl:if>
        </xsl:for-each>
    </xsl:template>
</xsl:stylesheet>

With this input:

<list>
    <num>4</num>
    <num>9</num>
</list>

Output:

4

And this input:

<list>
    <num>1</num>
    <num>7</num>
</list>

Output:

7

EDIT: XSLT 2.0 solution:

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:param name="pValue" select="5"/>
    <xsl:template match="/">
        <xsl:variable name="vSequence"
                      select="/list/num/abs(. - $pValue)"/>
        <xsl:variable name="vMinimum"
                      select="min($vSequence)"/>
        <xsl:variable name="vPosition"
                      select="index-of($vSequence,$vMinimum)[1]"/>
        <xsl:value-of select="/list/num[$vPosition]"/>
    </xsl:template>
</xsl:stylesheet>

It shows that it could be one line XPath 2.0 expression:

/list/num[
   index-of(
      /list/num/abs(. - $pValue),
      min(/list/num/abs(. - $pValue))
   )[1]
]
  • As usual, you guys rock. I had to use the 1.0 implementation since I'm on msft. Thanks again. – Matt Feb 01 '11 at 15:27
  • @Matt: You are welcome. Do note that in .Net enviroment you can use Altova, XQSharp native .Net XSLT 2.0 processors. –  Feb 01 '11 at 16:05
1

I. XSLT 1.0 solution:

Because @Alejandro was quicker than I, I now had to devise another solution :)

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:ext="http://exslt.org/common" xmlns:my="my:my">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <my:num>5.6</my:num>

 <xsl:param name="pValue" select="document('')/*/my:num"/>

 <xsl:variable name="vList" select="/*/*"/>

 <xsl:template match="/">
   <xsl:variable name="vrtfSorted">
     <xsl:for-each select="$vList | $pValue">
      <xsl:sort select="." data-type="number"/>
      <xsl:copy-of select="."/>
     </xsl:for-each>
   </xsl:variable>

   <xsl:variable name="vSorted" select="ext:node-set($vrtfSorted)/*"/>

   <xsl:variable name="vVal1" select=
    "$vSorted[.=$pValue]/preceding-sibling::*[1]"/>

   <xsl:variable name="vVal2" select=
    "$vSorted[.=$pValue]/following-sibling::*[1]"/>

   <xsl:value-of select=
    "($pValue - $vVal1 > $vVal2 - $pValue) * $vVal2
     +
     (not($pValue - $vVal1 > $vVal2 - $pValue)) * $vVal1
    "/>
 </xsl:template>
</xsl:stylesheet>

when applied on the following XML document (containing the list of values):

<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:

6

II. XSLT 2.0 solution:

<xsl:stylesheet version="2.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:xs="http://www.w3.org/2001/XMLSchema">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:param name="pNumber" as="xs:double" select="5.3"/>
 <xsl:param name="pList" as="xs:double+"
  select="3,2,5,7,8,6,4,9,1"/>

 <xsl:template match="/">
  <xsl:variable name="vSorted" as="xs:double+">
   <xsl:perform-sort select="$pList">
    <xsl:sort select="abs(. - $pNumber)"/>
   </xsl:perform-sort>
  </xsl:variable>
  <xsl:sequence select="$vSorted[1]"/>
 </xsl:template>
</xsl:stylesheet>

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

5
Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431