1

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>
2
  • 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 Commented May 9, 2017 at 10:37
  • Which XSLT processor will you be using? You could really use some extension functions, if your processor supports them. Commented May 9, 2017 at 11:20

3 Answers 3

1

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>
Sign up to request clarification or add additional context in comments.

3 Comments

how to xslt1.0 please udate
@michael.hor257k please explain solution
@michael.hor257k, you right so I have deleted that comment.
0

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>

3 Comments

That algorithm fails comparing 1.1.20 to 1.2.1.
You are correct Michael, my test data didn't go that deep therefore I didn't see that. Thanks for pointing it out. I will delete the comment. I'm new to this, so don't know the full capability of xslt. Anyway of solving the dynamic key levels? As in the example it has 4 keys but it can even go to 10 or more.
Sorry, I had a bit of a think about it but XSLT 1.0 is just too limited to make this fun. I think you would have to do a "digit-by-digit" sort, sorting on successive keys starting at the last, which would involve some pretty hairy recursion, and then you would need more recursion to tokenize the actual key values. It can be done in 1.0 if you're really desperate (most things can) but I gave up using 1.0 about 15 years ago because things like this are just too painful.
0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.