views:

481

answers:

3
+4  Q: 

XSL recursive sort

Hello, I am facing a problem where I need to sort elements, depending on their value, which contains a numbers, separated by periods. I need to sort elements depending on the value of the number before first period, then the number between first and second periods and so on. I don't know, how deep this hierarchy can go and that is the biggest problem.

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <ROW>2.0.1</ROW>
    <ROW>1.2</ROW>
    <ROW>1.1.1</ROW>
    <ROW>1.2.0</ROW>
    <ROW>1</ROW>
</root>

The result shoul be like this:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <ROW>1</ROW>
    <ROW>1.1.1</ROW>
    <ROW>1.2</ROW>
    <ROW>1.2.0</ROW>
    <ROW>2.0.1</ROW>
</root>

Is this possible at all? Appreciate any help.

+4  A: 

there is an "easy" answer that doesn't use any extension: split row values into chucks and sort on it.

<xsl:template match="root">
    <xsl:copy>
     <xsl:apply-templates select="ROW">
      <xsl:sort select="substring-before(concat(., '.'), '.')" data-type="number"/>
     </xsl:apply-templates>
    </xsl:copy>
</xsl:template>
<xsl:template match="ROW">
    <xsl:param name="prefix" select="''"/>
    <xsl:choose>
     <!-- end of recursion, there isn't any more ROW with more chucks -->
     <xsl:when test=". = substring($prefix, 1, string-length($prefix)-1)">
      <xsl:copy-of select="."/>
     </xsl:when>
     <xsl:otherwise>
      <xsl:variable name="chuck" select="substring-before(concat(substring-after(., $prefix), '.'), '.')"/>
      <!-- this test is for grouping ROW with same prefix, to skip duplicates -->
      <xsl:if test="not(preceding-sibling::ROW[starts-with(., concat($prefix, $chuck))])">
       <xsl:variable name="new-prefix" select="concat($prefix, $chuck, '.')"/>
       <xsl:apply-templates select="../ROW[starts-with(., $new-prefix) or . = concat($prefix, $chuck)]">
        <xsl:sort select="substring-before(concat(substring-after(., $new-prefix), '.'), '.')" data-type="number"/>
        <xsl:with-param name="prefix" select="$new-prefix"/>
       </xsl:apply-templates>
      </xsl:if>
     </xsl:otherwise>
    </xsl:choose>
</xsl:template>
Erlock
+1  A: 

The only problem in accomplishing this is in dealing with individual numbers (between the periods) of different text lengths (i.e. sorting 1.0, 2.0 and 10.0 in that order). If there's an upper limit on the size of the individual numbers (say n digits) then create a sort key that is the concatenation of all the numbers zero-padded to n digits. For n=3, this results in

Row               Key (string)
1                 001
1.0               001000
1.0.1             001000001
1.1               001001
1.2.1             001002001
2.0.1             002000001
10.0.1            010000001

Then sort on the key. If you're stuck in XSLT 1.0 you'll have to resort to EXSLT extension functions to do the parsing and key normalization.

Jim Garrison
your "the only problem" is exactly the hard part.
ax
+1  A: 

It's not pretty but you can use the xalan:nodeset function to "pre-process" the numbers into a nodeset with an easily sortable expression as described by Jim.

This example works for me with Xalan 2.5.1:

<?xml version="1.0"?>

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
    xmlns:xalan="http://xml.apache.org/xalan"&gt;

<xsl:output method="xml" indent="yes" />

<xsl:template match="/">
 <root>
  <!-- Create a sort node with a sort expression wrapping each ROW -->
  <xsl:variable name="nodes">
   <xsl:for-each select="/root/ROW">
    <xsl:variable name="sort-string">
     <xsl:call-template name="create-sort-string">
      <xsl:with-param name="sort-string" select="text()" />
     </xsl:call-template>
    </xsl:variable>
    <sort sort-by="{$sort-string}">
     <xsl:copy-of select="." />
    </sort>
   </xsl:for-each>
  </xsl:variable>

  <!-- Now sort the sort nodes and copy out the ROW elements -->
  <xsl:for-each select="xalan:nodeset($nodes)/sort">
   <xsl:sort select="@sort-by" data-type="text" />
   <xsl:copy-of select="*" />
  </xsl:for-each>
 </root>
</xsl:template>

<xsl:template name="create-sort-string">
 <xsl:param name="sort-string" />
 <!-- Biggest number at each level -->
 <xsl:variable name="max-num" select="1000" />
 <xsl:choose>
  <xsl:when test="contains($sort-string, '.')">
   <xsl:value-of select="$max-num + number(substring-before($sort-string, '.'))" />
   <xsl:call-template name="create-sort-string">
    <xsl:with-param name="sort-string" select="substring-after($sort-string, '.')" />
   </xsl:call-template>
  </xsl:when>
  <xsl:otherwise>
   <xsl:value-of select="concat($max-num + number($sort-string), '0')" />
  </xsl:otherwise>
 </xsl:choose>
</xsl:template>

I personally think writing an extension function would be preferable, but I know that's it's not always an option.

Carlos da Costa