XSLT: replace before sorting

xslt

I am working with XSLT 1.0 (so I can't use the replace() function), and I need to make a replace in a string, before use that string for sorting. Briefly, my XML doc looks like this:

<root>
    <item>
        <name>ABC</name>
        <rating>good</rating>
    </item>
    <item>
        <name>BCD</name>
        <rating>3</rating>
    </item>
</root>

Then I need to replace 'good' with '4', in order to print the whole items list ordered by rating using the sort() function. Since I'm using XSLT 1.0, I use this template for replacements:

<xsl:template name="string-replace">
  <xsl:param name="subject"     select="''" />
  <xsl:param name="search"      select="''" />
  <xsl:param name="replacement" select="''" />
  <xsl:param name="global"      select="false()" />

  <xsl:choose>
    <xsl:when test="contains($subject, $search)">
      <xsl:value-of select="substring-before($subject, $search)" />
      <xsl:value-of select="$replacement" />
      <xsl:variable name="rest" select="substring-after($subject, $search)" />
      <xsl:choose>
        <xsl:when test="$global">
          <xsl:call-template name="string-replace">
            <xsl:with-param name="subject"     select="$rest" />
            <xsl:with-param name="search"      select="$search" />
            <xsl:with-param name="replacement" select="$replacement" />
            <xsl:with-param name="global"      select="$global" />
          </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
          <xsl:value-of select="$rest" />
        </xsl:otherwise>
      </xsl:choose>
    </xsl:when>
    <xsl:otherwise>
      <xsl:value-of select="$subject" />
    </xsl:otherwise>
  </xsl:choose>
</xsl:template>

This templates works fine, but the problem is that it always print the values, (i.e. always when I call the template something is printed). Therefore, this template is not usefull in this case, because I need to modify the 'rating' value, then sort the items by rating and finally print them.

Thanks in advance!

PS: A workaround would be use two different XSLT, but for several reasons I can't do it in this case.

Best Answer

You can do this:

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

  <xsl:template match="/root">
    <xsl:for-each select="item">
      <!-- be sure to include every possible value of <rating>! -->
      <xsl:sort select="
        concat(
          substring('4', 1, rating = 'good' ),
          substring('3', 1, rating = 'medioce' ),
          substring('2', 1, rating = 'bad' ),
          substring('1', 1, rating = 'abyssmal' ),
          substring('4', 1, rating = '4' ),
          substring('3', 1, rating = '3' ),
          substring('2', 1, rating = '2' ),
          substring('1', 1, rating = '1' )
        )
      " order="descending" />
      <xsl:copy-of select="." />
    </xsl:for-each>
  </xsl:template>
</xsl:stylesheet>

With an input of:

<root>
  <item>
    <name>ABC</name>
    <rating>abyssmal</rating>
  </item>
  <item>
    <name>GEH</name>
    <rating>bad</rating>
  </item>
  <item>
    <name>DEF</name>
    <rating>good</rating>
  </item>
  <item>
    <name>IJK</name>
    <rating>medioce</rating>
  </item>
</root>

I get:

<item>
  <name>DEF</name>
  <rating>good</rating>
</item>
<item>
  <name>IJK</name>
  <rating>medioce</rating>
</item>
<item>
  <name>GEH</name>
  <rating>bad</rating>
</item>
<item>
  <name>ABC</name>
  <rating>abyssmal</rating>
</item>

For an explanation, see my other answer. ;-)


EDIT

Changed solution upon this comment of the OP:

I need to use the rating (with the strings replaced by integer scores), 3 times:

  1. make a key with <xsl:key ... using the rating
  2. Sort the items using the rating
  3. Print the rating.

In each step I should use the rating AFTER the replace, (i.e. using integer scores). I have done it repeating the concat(...) code 3 times, but as you can see this is not too cool... I would like to find a way to place the concat (...) one time, without need to repeat it.

The following XSLT 1.0 solution fulfills all these requests:

<xsl:stylesheet 
  version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:tmp="http://tempuri.org/"
  exclude-result-prefixes="tmp"
>
  <xsl:output method="xml" encoding="utf-8" />

  <!-- prepare a list of possible ratings for iteration -->
  <tmp:ratings>
    <tmp:rating num="1" />
    <tmp:rating num="2" />
    <tmp:rating num="3" />
    <tmp:rating num="4" />
  </tmp:ratings>

  <!-- index items by their rating -->
  <xsl:key 
    name="kItemByRating" 
    match="item" 
    use="concat(
      substring('4', 1, rating = 'good' ),
      substring('3', 1, rating = 'medioce' ),
      substring('2', 1, rating = 'bad' ),
      substring('1', 1, rating = 'abyssmal' ),
      substring('4', 1, rating = '4' ),
      substring('3', 1, rating = '3' ),
      substring('2', 1, rating = '2' ),
      substring('1', 1, rating = '1' )
    )
  " />

  <!-- we're going to need that later-on -->
  <xsl:variable name="root" select="/" />

  <xsl:template match="/root">
    <!-- iterate on the prepared list of ratings -->
    <xsl:apply-templates select="document('')/*/tmp:ratings/tmp:rating">
      <xsl:sort select="@num" order="descending" />
    </xsl:apply-templates>
  </xsl:template>

  <xsl:template match="tmp:rating">
    <xsl:variable name="num" select="@num" />
    <!-- 
      The context node is part of the XSL file now. As a consequence,
      a call to key() would be evaluated within the XSL file.

      The for-each is a means to change the context node back to the 
      XML file, so that the call to key() can return <item> nodes.
    -->
    <xsl:for-each select="$root">
      <!-- now pull out all items with a specific rating -->
      <xsl:apply-templates select="key('kItemByRating', $num)">
        <!-- note that we use the variable here! -->
        <xsl:with-param name="num" select="$num" />
        <xsl:sort select="@name" />
      </xsl:apply-templates>
    </xsl:for-each>
  </xsl:template>

  <xsl:template match="item">
    <xsl:param name="num" select="''" />
    <xsl:copy>
      <!-- print out the numeric rating -->
      <xsl:attribute name="num">
        <xsl:value-of select="$num" />
      </xsl:attribute>
      <xsl:copy-of select="node() | @*" />
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>