7

I am experiiencing an issue with the < operator on strings in Xpath 1.0.

This simple Xpath expression

'A' < 'B' (or the equivalent 'A' &lt; 'B')

did not evaluate to true in my xslt run in libxslt (which is an XSLT 1.0 engine).

I checked in XML Spy, which allows testing Xpath expressions in both 1.0 and 2.0, and sure enough, in Xpath 2.0 it evaluates to true, but in Xpath 1.0 it evaluates to false!

Is this a bug in Xpath 1.0?

What other expression should I use to compare two strings/characters for their alphabetical order? Note that the compare() function will not do, as this is an XSLT 2.0 function.

hakre
  • 193,403
  • 52
  • 435
  • 836
Maestro13
  • 3,656
  • 8
  • 42
  • 72

4 Answers4

7

In XPath 1.0, string comparison is defined only for = and !=, and ordering comparisons are not available. The spec says

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.

Thus both your operands are being converted to float, making them both NaN.

I believe Microsoft's XML adds extension functions to handle this, but of course this helps only if you're using MSXML.

Jim Garrison
  • 85,615
  • 20
  • 155
  • 190
5

Yes, this is a limitation of XPath 1.0. (I don't think it's reasonable to refer to a limitation you don't like as a "bug", though clearly the designers of XPath 2.0 agreed with you that it was an undesirable limitation).

You've tagged your question "xslt", so you may be able to work around the problem at the XSLT level, at least if your processor has the node-set extension:

<xsl:variable name="nodes">
  <node><xsl:value-of select="$A"/></node>
  <node><xsl:value-of select="$B"/></node>
</xsl:variable>

<xsl:for-each select="exslt:node-set($nodes)/*">
  <xsl:sort select="."/>
  <xsl:if test="position()=1 and .=$A">A comes first!</xsl:if>
</xsl:for-each>

But perhaps it's time to move to 2.0. What's holding you back?

Michael Kay
  • 156,231
  • 11
  • 92
  • 164
  • Thanks Michael - nice compact solution. As to XSLT 2.0 what's holding me back - `libxslt` is - that's the engine used by `php 5` and I can't change that. Maybe in the future my hosting servicer will go use a php version that uses an XSLT 2.0 engine - when there is one. I really would have liked to do all this in XSLT 2.0 of course - in fact I did for the development and then had to rewrite everything. I assume the same reason for not moving to XSLT 2.0 holds for a great number of XSLT developers. – Maestro13 Jun 21 '12 at 05:58
  • @Maestro13: Zobra supports XPath 2.0 and is available as a PHP extension, see: http://www.ibm.com/developerworks/xml/library/x-zorba/index.html - As far as PHP is concerned you can also [register PHP functions](http://php.net/manual/en/domxpath.registerphpfunctions.php) like `strcmp` to be used with your xpath. – hakre Jun 21 '12 at 07:58
  • @hakre thanks for the info - I will go check with my hosting service provider whether they can activate Zobra. And yes an alternative would be to register a custom php function and use that in the xslt - in the heat of the XSLT battle I totally forgot that. – Maestro13 Jun 21 '12 at 08:16
2

In the hope that this proves to be useful to others too, below is the code I wrote following Michael Kay's suggestion. I wrote a custom compare function that gives the same results as Xpath 2.0's one. I also added the php tag to the question so that it will be found more often.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
    version="1.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:func="http://exslt.org/functions"
    xmlns:common="http://exslt.org/common"
    xmlns:custom="urn:myCustomFunctions"
    exclude-result-prefixes="func common custom" 
    extension-element-prefixes="func custom">

    <xsl:output method="xml"/>

    <func:function name="custom:compare">
        <xsl:param name="string1"/>
        <xsl:param name="string2"/>

        <func:result>
            <xsl:choose>
                <xsl:when test="$string1 = $string2">0</xsl:when>
                <xsl:otherwise>
                    <xsl:variable name="nodes">
                        <node><xsl:value-of select="$string1"/></node>
                        <node><xsl:value-of select="$string2"/></node>
                    </xsl:variable>
                    <xsl:for-each select="common:node-set($nodes)/*">
                        <xsl:sort select="."/>
                        <xsl:choose>
                            <xsl:when test="position()=1 and .=$string1">-1</xsl:when>
                            <xsl:when test="position()=1 and .=$string2">1</xsl:when>
                        </xsl:choose>
                    </xsl:for-each>
                </xsl:otherwise>
            </xsl:choose>
        </func:result>
    </func:function>

    <xsl:template match="/">
        <out>
            <test1><xsl:value-of select="custom:compare('A', 'B')"/></test1>
            <test2><xsl:value-of select="custom:compare('A', 'A')"/></test2>
            <test3><xsl:value-of select="custom:compare('C', 'B')"/></test3>
            <test4><xsl:value-of select="custom:compare('DD', 'A')"/></test4>
        </out>
    </xsl:template>

</xsl:stylesheet>

The result of running this (with dummy input) is

<?xml version="1.0"?>
<out>
    <test1>-1</test1>
    <test2>0</test2>
    <test3>1</test3>
    <test4>1</test4>
</out>

For those who wish to test this in php for themselves, here's the code I used:

<?php 
$xslt = new XSLTProcessor();
$xslt->importStylesheet( DOMDocument::load('testCompare.xslt') );
$xslt -> registerPHPFunctions();
$xml = new SimpleXMLElement('<test/>'); 
print $xslt->transformToXML( $xml );
?>
Maestro13
  • 3,656
  • 8
  • 42
  • 72
1

It might be ugly solution, and not feasible in many situations, but for simple alphabetical order comparison you can use translate. The following snippet is just an example that can be extended furtherly:

  translate('A','ABCD','1234') &lt; translate('B','ABCD','1234');

Your translate expression should cover all letters, up and low cases, and could be conveniently reused by defining a named template.

Emiliano Poggi
  • 24,390
  • 8
  • 55
  • 67