9

Background

I was recently surprised to notice that XSL was able to intelligently handle numbers; i.e. knowing to treat numbers in text as numeric when performing comparisons (i.e. it understood that 7 < 10 rather than thinking '10' < '7'). In my case that's what I wanted; just not what I'd expected.

Out of curiosity I then tried to force XSLT to compare the numbers as strings (i.e. by using the string() function, but with no luck.

Question

Is it possible to get XSLT to compare numbers as strings; e.g. so '10' < '7'?

Example

Source XML:

<?xml version="1.0" encoding="utf-8"?>
<element>
  <x>1</x>
  <x>2</x>
  <x>3</x>
  <x>4</x>
  <x>5</x>
  <x>6</x>
  <x>7</x>
  <x>8</x>
  <x>9</x>
  <x>10</x>
</element>

XSLT:

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

      <AsItComes>
        <xsl:for-each select="./x">
          <xsl:if test="./text() &lt; 7">
            <xsl:copy-of select="."></xsl:copy-of>
          </xsl:if>
        </xsl:for-each>
      </AsItComes>

      <AsNumber>
      <xsl:for-each select="./x">
        <xsl:if test="number(./text()) &lt; 7">
          <xsl:copy-of select="."></xsl:copy-of>
        </xsl:if>
      </xsl:for-each>
      </AsNumber>

      <AsString>
        <xsl:for-each select="./x">
          <xsl:if test="string(./text()) &lt; '7'">
            <xsl:copy-of select="."></xsl:copy-of>
          </xsl:if>
        </xsl:for-each>
      </AsString>

    </element>
  </xsl:template>
</xsl:stylesheet>

Expected Output:

<?xml version="1.0" encoding="utf-8"?>
<element>
  <AsItComes>
    <x>1</x>
    <x>2</x>
    <x>3</x>
    <x>4</x>
    <x>5</x>
    <x>6</x>
    <x>10</x>
  </AsItComes>
  <AsNumber>
    <x>1</x>
    <x>2</x>
    <x>3</x>
    <x>4</x>
    <x>5</x>
    <x>6</x>
  </AsNumber>
  <AsString>
    <x>1</x>
    <x>2</x>
    <x>3</x>
    <x>4</x>
    <x>5</x>
    <x>6</x>
    <x>10</x>
  </AsString>
</element>

Actual Output:

<?xml version="1.0" encoding="utf-8"?>
<element>
  <AsItComes>
    <x>1</x>
    <x>2</x>
    <x>3</x>
    <x>4</x>
    <x>5</x>
    <x>6</x>
  </AsItComes>
  <AsNumber>
    <x>1</x>
    <x>2</x>
    <x>3</x>
    <x>4</x>
    <x>5</x>
    <x>6</x>
  </AsNumber>
  <AsString>
    <x>1</x>
    <x>2</x>
    <x>3</x>
    <x>4</x>
    <x>5</x>
    <x>6</x>
  </AsString>
</element>
JohnLBevan
  • 22,735
  • 13
  • 96
  • 178

3 Answers3

6

It appears that in XSLT/XPATH 1.0, the string() value is still evaluated as a number when performing the comparison.

https://www.w3.org/TR/xpath/#booleans

When neither object to be compared is a node-set and the operator is <=, <, >= or >, then the objects are compared by converting both objects to numbers and comparing the numbers according to IEEE 754. The < comparison will be true if and only if the first number is less than the second number.

With XSLT/XPATH 2.0 (and 3.0, and 3.1), you can explicitly set the data type as xs:string to ensure that the comparison is performed against strings and not coerced into number values.

<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="element">
    <element>
      <AsString>
        <xsl:for-each select="./x">
          <xsl:if test="xs:string(.) &lt; xs:string('7')">
            <xsl:copy-of select="."></xsl:copy-of>
          </xsl:if>
        </xsl:for-each>
      </AsString>
    </element>
 </xsl:template>
</xsl:stylesheet>

But it is sufficient to compare the value to the string '7' (also, you could eliminate the <xsl:if> and put your filter in a predicate):

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
            version="2.0">
 <xsl:template match="element">
    <element>
      <AsString>
        <xsl:for-each select="./x[. &lt; '7']">
          <xsl:copy-of select="."></xsl:copy-of>
        </xsl:for-each>
      </AsString>
    </element>
 </xsl:template>
</xsl:stylesheet>
Mads Hansen
  • 63,927
  • 12
  • 112
  • 147
  • 2
    I agree on the analysis, but for XSLT 2.0 it suffices to use `` with a `version="2.0"` stylesheet, suggesting `` I fear people think they have to wrap anything in `xs:string` constructor calls. – Martin Honnen Jan 12 '16 at 16:10
2

if you are going for the first number a work-around could be to just substring the first position.

<xsl:if test="substring(./text(), 1, 1) &lt; '7'">

returns

<AsString>
  <x>1</x>
  <x>2</x>
  <x>3</x>
  <x>4</x>
  <x>5</x>
  <x>6</x>
  <x>10</x>
</AsString>
Martin Vitek
  • 159
  • 4
  • 1
    Thanks @MartinVitek; good workaround. I'm really interested in whether a full text comparison is possible; though only out of curiosity rather than any necessity / requirement. – JohnLBevan Jan 12 '16 at 15:28
1

Note that in XSLT 1.0 both 'a' > 'b' and 'b' > 'a' evaluate as false.

michael.hor257k
  • 113,275
  • 6
  • 33
  • 51