XSLT Compare Numbers as Strings
Asked Answered
S

3

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>
Sagerman answered 12/1, 2016 at 14:56 Comment(0)
T
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>
Teeterboard answered 12/1, 2016 at 15:39 Comment(1)
I agree on the analysis, but for XSLT 2.0 it suffices to use <xsl:if test=". &lt; '7'"> with a version="2.0" stylesheet, suggesting <xsl:if test="xs:string(.) &lt; xs:string('7')"> I fear people think they have to wrap anything in xs:string constructor calls.Protochordate
W
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>
Waterresistant answered 12/1, 2016 at 15:21 Comment(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.Sagerman
G
1

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

Geoid answered 12/1, 2016 at 15:58 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.