1
votes

The output xml doesn't sort as expected when I use the xsl:sort.

As you can see in the result section the 1.1.1.10, 2.10, 2.11, 2.12 are not correctly sorted.

I would expect the sorted lists to be as shown in the expected result xml.

Am I doing something wrong here? Please help.

Input XML

    <children>
        <child name="1.1.1.1 BDR Enter Customer"  prefix="1.1.1.1"/>
        <child name="1.1.1.10 BDR for Tax Office"  prefix="1.1.1.10"/>
        <child name="1.1.1.2 BDR Enter Customs"  prefix="1.1.1.2"/>
        <child name="1.1.1.3 BDR Enter Employee"  prefix="1.1.1.3"/>
        <child name="1.1.1.4 BDR Enter Forwarder"  prefix="1.1.1.4"/>
        <child name="1.1.1.5 BDR Enter Manufacturer"  prefix="1.1.1.5"/>
        <child name="1.1.1.6 BDR Enter Owner"  prefix="1.1.1.6"/>
        <child name="1.1.1.7 BDR Enter Person"  prefix="1.1.1.7"/>
        <child name="1.1.1.8 BDR Enter Resource"  prefix="1.1.1.8"/>
        <child name="1.1.1.9 BDR Enter Supplier"  prefix="1.1.1.9"/>
        <child name="1.1 Define System Basics" prefix="1.1"/>
        <child name="2.9 Set Up Basic Data Accounting Rules" prefix="2.9"/>
        <child name="2.2 Budget Management"  prefix="2.2"/>
        <child name="2.10 Customer Bill of Exchange Payment"  prefix="2.10"/>
        <child name="2.5 Depreciation Plan"  prefix="2.5"/>
        <child name="2.12 Supplier Credit Invoice"  prefix="2.12"/>
        <child name="2.11 AP/AR Nettings"  prefix="2.11"/>
        <child name="2.3 Arrival Entry Supplier Invoice"  prefix="2.3"/>
        <child name="2.1 Archive Data, Internal Ledger"  prefix="2.1"/>
</children>

My XSL

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output method="xml" encoding="utf-8" indent="yes"/>
    <xsl:template match="children">
        <children>
            <xsl:for-each select="child">
                <xsl:sort select="@prefix"/>
                <child>
                    <xsl:attribute name="name">
                        <xsl:value-of select="@name"/>
                    </xsl:attribute>
                    <xsl:attribute name="prefix">
                        <xsl:value-of select="@prefix"/>
                    </xsl:attribute>
                </child>
            </xsl:for-each>
        </children>
    </xsl:template>
</xsl:stylesheet>

Result

<?xml version="1.0" encoding="utf-8"?>
<children>
  <child name="1.1 Define System Basics" prefix="1.1"/>
  <child name="1.1.1.1 BDR Enter Customer" prefix="1.1.1.1"/>
  <child name="1.1.1.10 BDR for Tax Office" prefix="1.1.1.10"/>
  <child name="1.1.1.2 BDR Enter Customs" prefix="1.1.1.2"/>
  <child name="1.1.1.3 BDR Enter Employee" prefix="1.1.1.3"/>
  <child name="1.1.1.4 BDR Enter Forwarder" prefix="1.1.1.4"/>
  <child name="1.1.1.5 BDR Enter Manufacturer" prefix="1.1.1.5"/>
  <child name="1.1.1.6 BDR Enter Owner" prefix="1.1.1.6"/>
  <child name="1.1.1.7 BDR Enter Person" prefix="1.1.1.7"/>
  <child name="1.1.1.8 BDR Enter Resource" prefix="1.1.1.8"/>
  <child name="1.1.1.9 BDR Enter Supplier" prefix="1.1.1.9"/>
  <child name="2.1 Archive Data, Internal Ledger" prefix="2.1"/>
  <child name="2.10 Customer Bill of Exchange Payment" prefix="2.10"/>
  <child name="2.11 AP/AR Nettings" prefix="2.11"/>
  <child name="2.12 Supplier Credit Invoice" prefix="2.12"/>
  <child name="2.2 Budget Management" prefix="2.2"/>
  <child name="2.3 Arrival Entry Supplier Invoice" prefix="2.3"/>
  <child name="2.5 Depreciation Plan" prefix="2.5"/>
  <child name="2.9 Set Up Basic Data Accounting Rules" prefix="2.9"/>
</children>

Expected Result

<?xml version="1.0" encoding="utf-8"?>
<children>
  <child name="1.1 Define System Basics" prefix="1.1"/>
  <child name="1.1.1.1 BDR Enter Customer" prefix="1.1.1.1"/>
  <child name="1.1.1.2 BDR Enter Customs" prefix="1.1.1.2"/>
  <child name="1.1.1.3 BDR Enter Employee" prefix="1.1.1.3"/>
  <child name="1.1.1.4 BDR Enter Forwarder" prefix="1.1.1.4"/>
  <child name="1.1.1.5 BDR Enter Manufacturer" prefix="1.1.1.5"/>
  <child name="1.1.1.6 BDR Enter Owner" prefix="1.1.1.6"/>
  <child name="1.1.1.7 BDR Enter Person" prefix="1.1.1.7"/>
  <child name="1.1.1.8 BDR Enter Resource" prefix="1.1.1.8"/>
  <child name="1.1.1.9 BDR Enter Supplier" prefix="1.1.1.9"/>
  <child name="1.1.1.10 BDR for Tax Office" prefix="1.1.1.10"/>
  <child name="2.1 Archive Data, Internal Ledger" prefix="2.1"/>
  <child name="2.2 Budget Management" prefix="2.2"/>
  <child name="2.3 Arrival Entry Supplier Invoice" prefix="2.3"/>
  <child name="2.5 Depreciation Plan" prefix="2.5"/>
  <child name="2.9 Set Up Basic Data Accounting Rules" prefix="2.9"/>
  <child name="2.10 Customer Bill of Exchange Payment" prefix="2.10"/>
  <child name="2.11 AP/AR Nettings" prefix="2.11"/>
  <child name="2.12 Supplier Credit Invoice" prefix="2.12"/>
</children>
3
Sorting is based on strings and that's how they order when doing that. Having a numerical sorting with parts split out isn't part of XSLT, might be possible to implement somehow though - Sami Kuhmonen
Which XSLT processor will you be using? You could really use some extension functions, if your processor supports them. - michael.hor257k

3 Answers

1
votes

Using XSLT 3.0 and the sort function from XPath 3.1 you could do it with

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:math="http://www.w3.org/2005/xpath-functions/math"
    exclude-result-prefixes="xs math"
    version="3.0">

    <xsl:mode on-no-match="shallow-copy"/>

    <xsl:output indent="yes"/>

    <xsl:template match="children">
        <xsl:copy>
            <xsl:apply-templates select="sort(child, (), function($c) { tokenize($c/@prefix, '\.')!xs:integer(.) })"/>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

Saxon 9.7 PE or EE and recent versions of Altova XMLSpy or Raptor support XSLT 3.0.

Using Saxon 9 (including HE) you can do it with XSLT 2.0 and collation="http://saxon.sf.net/collation?alphanumeric=yes" on the xsl:sort:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="xs"
    version="2.0">

    <xsl:output indent="yes"/>

    <xsl:template match="@* | node()">
        <xsl:copy>
            <xsl:apply-templates select="@* | node()"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="children">
        <xsl:copy>
            <xsl:apply-templates select="child">
                <xsl:sort select="@prefix" collation="http://saxon.sf.net/collation?alphanumeric=yes"/>
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>
1
votes

Am I doing something wrong here?

Yes, you are doing two things wrong:

  • In the first place, you are sorting by a single sort key, when in fact you have four sort keys (at least) -- the separate segments of @prefix delimited by '.' characters.

  • In the second place, you appear to want a numeric sort by each key, but you are instead accepting the default lexicographic sort.

In XSLT, you can sort by multiple keys by providing multiple xsl:sort elements, and you can get numeric sorting via the data-type attributes of those elements. If you are restricted to XSLT 1.0 with no extensions, however, then that still leaves you with a slightly tricky problem of breaking up your prefix attribute into keys. For a fixed maximum number of segments, you can do it like this:

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

  <xsl:template match="node()|@*">
    <xsl:copy>
       <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="children">
    <xsl:copy>
      <xsl:apply-templates select="child">
        <xsl:sort data-type="number"
            select="substring-before(@prefix, '.')"/>
        <xsl:sort data-type="number"
            select="substring-before(substring-after(concat(@prefix, '.0.0.0.'), '.'), '.')"/>
        <xsl:sort data-type="number"
            select="substring-before(substring-after(substring-after(concat(@prefix, '.0.0.0.'), '.'), '.'), '.')"/>
        <xsl:sort data-type="number"
            select="substring-before(substring-after(substring-after(substring-after(concat(@prefix, '.0.0.0.'), '.'), '.'), '.'), '.')"/>
      </xsl:apply-templates>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

Note the use of the substring-before() and substring-after() functions to pick apart the prefix at the '.' delimiters, and the concatenation of '.0.0.0.' to the prefix to ensure that all prefixes are treated the same, regardless of depth, up to the maximum supported depth (four in this case, but that could be extended to whatever depth is needed).

Note also that xsl:copy is usually to be preferred over a literal output element, xsl:element, or xsl:attribute where the former is applicable, and that sorting can be applied to xsl:apply-templates, too, not just xsl:for-each.

0
votes

If there really is no way to do this either by moving forward to a newer XSLT version or by exploiting vendor extensions, then the way I would tackle it is to preprocess the XML modifying the content from

<child name="1.1.1.10 BDR for Tax Office" prefix="1.1.1.10"/>

to

<child name="1.1.1.10 BDR for Tax Office" prefix="1.1.1.10"
  k1="1" k2="1" k3="1" k4="10"/>

and then sort using a multi-part sort key:

<xsl:sort select="@k1" data-type="number"/>
<xsl:sort select="@k2" data-type="number"/>
<xsl:sort select="@k3" data-type="number"/>
<xsl:sort select="@k4" data-type="number"/>

The preprocessing can be done using a sequence of instructions such as

<xsl:attribute name="k2">
  <xsl:value-of select="substring-before(substring-after(@prefix, '.'), '.')"/>
</xsl:attribute>