tags:

views:

110

answers:

3

Hello,

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.

+1  A: 

If you only have a small set of pre-defined 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>

Using document('') is a trick that allows you to access a node inside your stylesheet document. In our case this is a set of nodes specifying the replacements to be made.

Using | rating in the select attribute of the xsl:sort element is another trick. It means that the result of the select expression is the union of the document('')/xsl:stylesheet/myExt:replacements/item[value=current()/rating]/replacement and rating. When the select expression is evaluated, only the first element of the resulting node set is considered. This has the effect that if there is no replacement defined, the value of the rating element will be used.

This is how the output document will look for you sample input:

<?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>
0xA3
That looks complex but fine (maybe too complex for me), I'm going to check it, and I will tell you something. A lot of thanks!
jävi
This is +1 for the effort alone. Nice approach, sometimes you can't get around doing it this way - `concat()` hackery is not always an option. ;-)
Tomalak
+2  A: 

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>
Tomalak
Beat me to it! I think this is simpler than the other example.
Chris R
+1, nice approach, but as I understood the question there can be both numeric and textual ratings in the same document so you would also have to consider the numeric ratings.
0xA3
There's a solution for everything, hang on. ;-)
Tomalak
Just edited mine to show one way...
Chris R
@jävi: If the number of possible `<rating>` values is finite then it's easiest to mention all values in the `concat()`. If it contains unknown values, these end up at the start/end of the list because the expression returns the empty string for them. Tell me if you need more than that.
Tomalak
Both solutions works perfectly, (in fact are very similar). Thank you too :)There is only one thing left, (explained in my comment in the other solutioon).Stackoverflow is fucking awesome :)
jävi
@Tomalak: one question, what is this?:xmlns:tmp="http://tempuri.org/"I have noticied that without that, the code doesn't work. Also I would like to print HTML instead XML nodes, what changes I should do?Thank you again...
jävi
It is the declaration of a temporary XML namespace. Without it, the `<tmp:ratings>` elment would work. You could change to `<xsl:output method="html" ...>`, but it's best you look at all the options `<xsl:output>` offers - `omit-xml-declaration` would be an alternative.
Tomalak
Yes, I come here in order to answer myself with exactly the same :).Thank you very much Tomalak. Now I have exactly what I was wanting :)
jävi
You are welcome. ;-) P.S.: In my last comment, I meant to say "would *not* work", of course.
Tomalak
+1  A: 

If you only need to replace the good rating with 4 then try this. It will replace the good rating with 4 for the purpose of the sort and leave all ratings that are not good as they are. The extra spaces are to 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 multiple 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'))
          )
        )

The boolean either gets converted to 1 for true or 0 for false. This is then used in the substring so only the one that is true will substring with length 1, the others will substring with a length of 0. Concatenating these all together leaves you with the replacement value.

Chris R
Nice thinking. ;-)
Tomalak
Cheers. Got the idea from your answer to my last question :) Seriously though, 7 seconds, I thought I had this one in the bag!
Chris R
Chris Reynolds: The last option (`not(boolean(rating = 'bad') or boolean(...`) is wrong - you forgot taking the `string-length()` into account. ;-)
Tomalak
@Tomalak I was assuming numeric ratings would only be 1 digit long. If this is not the case you are right, it would need `* string-length(rating)` at the end of this line.
Chris R
Awesome! that works perfectly!Theres is only one thing more. I need tu use the rating (with the strings replaced by integer scores), 3 times:1. make a key with <xsl:key ... using the rating2. Sort the items using the rating3. 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.Thanks you very very much guys :D
jävi
Thinking... I could use a template in order to avoid repeat the concat(...) code, but... it is impossible to 'modify' the value of the rating for the whole XLST?. I mean, it is impossible to modify the XML document that XLST has in memory when is running?
jävi
@jävi: I have an idea for that. Will return after lunch. ;-)
Tomalak
@Tomalak, you are the man! :) I wait for it!
jävi