tags:

views:

52

answers:

3

I need to change the order in which my for-each is executed only if some conditions are met.

Here is what my XML looks like :

<OptionList>
    <Option name="My First Option" />
    <Option name="My Second Option" />
</OptionList>

However, in some case, my XML can be like this :

<OptionList>
    <Option />
    <Option name="My Second Option" />
</OptionList>

In my XSL, I'm doing a for-each like this :

<xsl:for-each select="//OptionList/Option">
    {...}
</xsl:for-each>

I know I can change orders of Option nodes using this line in my for-each :

<xsl:sort select="position()" data-type="number" order="descending" />

The problem is that I want my order to be descending only when my first Option node is empty and doesn't have the name attribute. Otherwise, I want to keep the default ascending order.

Any input on how I can acheive that? So far, everything I tried ended up with "Variable out of scope" or invalid use of xpath functions.

A: 

Check if the first option has the name node or not

<xsl:if test="Option/[position()=1]/@name">sort here</xsl:test>
alopix
Note that an `xsl:sort` node may not be contained in an `xsl:if` node.
0xA3
+2  A: 

You can use a hack to change the sort order based on a condition:

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

  <xsl:template match="/">
    <result>
    <xsl:for-each select="//OptionList/Option">
      <xsl:sort data-type="number" order="ascending"
        select="position()*(-2*number(not(//OptionList/Option[1]/@name))+1)"/>

      <option>
        <xsl:value-of select="@name"/>
      </option>
    </xsl:for-each>
    </result>

  </xsl:template>
</xsl:stylesheet>

The hack is that number((true()) returns 1 and number(false()) returns 0. As a consequence, the expression

-2 * number(not(//OptionList/Option[1]/@name)) + 1

evaluates to 1 if the first option element has a name attribute and to -1 otherwise. This is used as a factor to reverse the sort order.

0xA3
+1 from me, because this is almost the exact solution I independently came to minutes ago. :)
Dimitre Novatchev
+1 for reverse calculation. But the path is wrong. Inside the `xsl:sort/@select` the context node is every `Option` element, so the relative path should be `../Option[1]/@name`. Also, I think you should say that this can be done (more verbose) with `xsl:choose`, and that would be the rigth choise if the condition (wich it's been evaluated for every node) became ineficient.
Alejandro
I'm not an expert in xsl but this one seems to work best. In the end, it would be easier if it was possible to put a sort in a if! Or have the sort done directly in the xml. Thanks for your help!
Gabriel
A: 

The order of producing the wanted output can be easily specified with <xsl:sort>:

This is natural and easy and involves almost no hacks.

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:variable name="vOrder" select=
   "2*boolean(/*/Option[1]/@name)-1"/>

 <xsl:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="/*">
   <xsl:copy>
     <xsl:for-each select="Option">
       <xsl:sort data-type="number" select="$vOrder* position()"/>

       <xsl:apply-templates select="."/>
     </xsl:for-each>
   </xsl:copy>
 </xsl:template>
</xsl:stylesheet>

When this transformation is performed on the this XML document:

<OptionList>
    <Option name="My First Option" />
    <Option name="My Second Option" />
    <Option name="My Third Option" />
</OptionList>

the wanted, correct result is produced:

<OptionList>
   <Option name="My First Option"/>
   <Option name="My Second Option"/>
   <Option name="My Third Option"/>
</OptionList>

When the same transformation is now performed on this XML document:

<OptionList>
    <Option />
    <Option name="My Second Option" />
    <Option name="My Third Option" />
</OptionList>

the wanted, correct result is produced again:

<OptionList>
   <Option name="My Third Option"/>
   <Option name="My Second Option"/>
   <Option/>
</OptionList>

Explanation: The variable $vOrder is defined in such a way, that it is -1 iff the first Option element has no name attribute and it is +1 if the first Option element has a name attribute. Here we use the fact that false() is converted automatically to 0 and true() to 1.

We also use the fact that when the sign of each number in sequence of increasing positive numbers (the positions) is reversed, the order of the new sequence becomes decreasing.

Dimitre Novatchev
This solution will not reverse the sort order of the option list, it will just put any empty option nodes at the end of the list. I read the question differently, I understood that the entire list should be reversed in case the first option element does not have a name attribute. Of course, if the list only has two items the result would be the same.
0xA3
@0xA3: Thank you for alerting me to this. I spent ten minutes to solve the new problem, saved it and ... saw that it is almost exact copy of yours... :(. Sorry.
Dimitre Novatchev