XSLT: replace before sorting

I am working with XSLT 1.0 (so I cannot use the replace () function) and I need to do the replacement in a string before using that string for sorting. In a nutshell, my XML document 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" to print out a list of all items, sorted by rating, using the sort () function . Since I am using XSLT 1.0, I am using this replacement pattern:

<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>

      

These templates work fine, but the problem is that it always prints values ​​(i.e. whenever I call the template, something prints). So this template won't come in handy in this case, because I need to change the "rating" value, then sort the items by rating, and finally print them.

Thanks in advance!

PS: A workaround would be to use two different XSLTs, but for several reasons I cannot do that in this case.

+2


source to share


3 answers


You can do it:

<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>

      

When typing:

<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>

      

See my other answer for an explanation .; -)




EDIT

Modified solution on this OP comment:

I need to use rating (with strings replaced with integers), 3 times:

  • enter key c <xsl:key ...

    using rating
  • Sorting items using rating
  • Print the rating.

At each step I have to use the rating AFTER replacement (i.e. using integer scores). I did this by repeating the concat(...)

code 3 times, but since you can see that this is not too cool ... I would like to find a way to post concat (...)

once, without having to repeat.

The following XSLT 1.0 solution satisfies all of 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>

      

+2


source


If you only have a small set of predefined replacements, you can use the following approach:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:myExt="http://www.example.com/myExtension"
    exclude-result-prefixes="myExt">

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

    <myExt:replacements>
      <item>
        <value>good</value>
        <replacement>4</replacement>
      </item>
      <item>
        <value>very good</value>
        <replacement>5</replacement>
      </item>
    </myExt:replacements>

    <xsl:template match="root">
      <out>
         <xsl:for-each select="item">
           <xsl:sort select="number(document('')/xsl:stylesheet/myExt:replacements/item[value=current()/rating]/replacement | rating)" order="ascending"/>
           <item>
             <name>
               <xsl:value-of select="name"/>
             </name>
             <rating>
               <xsl:value-of select="document('')/xsl:stylesheet/myExt:replacements/item[value=current()/rating]/replacement | rating"/>
             </rating>
           </item>
         </xsl:for-each>
       </out>
    </xsl:template>

      

Usage document('')

is a trick that allows you to access a node inside your stylesheet document. In our case, this is a set of nodes that define a replacement.



Using an | rating

element in the select attribute xsl:sort

is another trick. This means that the result of the select expression is the union of document('')/xsl:stylesheet/myExt:replacements/item[value=current()/rating]/replacement

and rating

. When the select expression is evaluated, only the first element of the result set node is considered. This causes the element's value to be used if no replacement is defined rating

.

This is how the sample input will appear in the output:

<?xml version="1.0" encoding="utf-8"?>
<out>
  <item>
    <name>BCD</name>
    <rating>3</rating>
  </item>
  <item>
    <name>ABC</name>
    <rating>4</rating>
  </item>
</out>

      

+1


source


If you only need to replace a good rating with 4, try this. It will replace the good rating with 4 for the purpose of sorting, and leave any ratings that are not as good as they are. Extra spaces make it easier to read / understand.

<xsl:for-each select="item">
    <xsl:sort select="
        concat(
          substring(
            '4', 
            1, 
            boolean(rating = 'good')
          ),
          substring(
            rating, 
            1, 
            not(boolean(rating = 'good'))
          )
        )
    "/>
</xsl:for-each>

      

If you need to replace several ratings, but some are already numeric, you can do the following:

        concat(
          substring(
            '4', 
            1, 
            boolean(rating = 'good')
          ),
          substring(
            '3', 
            1, 
            boolean(rating = 'average')
          ),
          substring(
            '2', 
            1, 
            boolean(rating = 'bad')
          ),
          substring(
            rating, 
            1, 
            not(boolean(rating = 'bad') or boolean(rating = 'average') or boolean(rating = 'good'))
          )
        )

      

A Boolean value is either converted to 1 for true or 0 for false. This is then used in a substring, so only the one that is true will be a substring with length 1, the rest will be substrings with length 0. Combining these all together leaves you with a replacement value.

+1


source







All Articles