tags:

views:

39

answers:

4

What is an XSLT 2.0 stylesheet that will transform

<paramList>
    <param name="y" out="true"/>
    <param name="y" in="true"/>
    <param name="z" out="true"/>
    <param name="x" in="true"/>
</paramList>

into

<paramList>
    <param name="x" in="true" />
    <param name="y" in="true" out="true"/>
    <param name="z" out="true"/>
</paramList>

In the result, "in, only" parameters precede "in & out" parameters, which, in turn, precede "out, only" parameters. Also, the two "y" elements have been combined into one.

+1  A: 
<?xml version="1.0" ?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;

<xsl:template match="/paramList">
  <xsl:copy>
    <xsl:for-each-group select="param" group-by="@name">
      <xsl:sort select="current-group()/@in" order="descending"/>
      <xsl:sort select="current-group()/@out"/>
      <param name="{current-grouping-key()}">
        <xsl:for-each select="current-group()/@*">
          <xsl:copy/>
        </xsl:for-each>
      </param>
    </xsl:for-each-group>
  </xsl:copy>
</xsl:template>

</xsl:stylesheet>
Nick Jones
Thank you very much, Nick! It worked fine for me.
JaysonFix
@Nick Jones: Why do you care about ordering attributes by name? Compact code: `<xsl:for-each-group select="param" group-by="@name"> <param> <xsl:copy-of select="current-group()/@*"/> </param> </xsl:for-each-group>`
Alejandro
Nick Jones
@Alejandro: Although I agree it is better to copy all the attributes as you suggest.
Nick Jones
@Nick Jones: Sorry, miss that in question. But then, I think you could also sort be `count(@in)*-1+count(@out)*2`. By the way +1 for a good solution.
Alejandro
A: 

Just in case someone needs to do it in XSLT 1:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
    <xsl:output method="xml" indent="yes"/>

    <xsl:key name="paramsByName" match="param" use="@name"/>
    <xsl:template match="/paramList">
        <xsl:copy>
            <xsl:for-each select="param[count(. | key('paramsByName', @name)[1]) = 1]">
            <xsl:sort select="@name"/>
            <xsl:copy>
                <xsl:for-each select="key('paramsByName', @name)">
                <xsl:copy-of select="@*"/>
            </xsl:for-each>
            </xsl:copy>
        </xsl:for-each>
    </xsl:copy>
    </xsl:template>
</xsl:stylesheet>

This uses Munchian grouping as XSLT 1 does not have a grouping construct.

Edit:

It is obviously also possible to just copy the in and out attributes in this case the following style sheet does the job (also following Dimetre's suggestions:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
    <xsl:output method="xml" indent="yes"/>

    <xsl:key name="paramsByName" match="param" use="@name"/>
    <xsl:template match="/paramList">
        <xsl:copy>
            <xsl:for-each select="param[count(. | key('paramsByName', @name)[1]) = 1]">
            <xsl:sort select="@name"/>
            <xsl:copy-of select="key('paramsByName', @name)/@*[local-name() = 'in' or local-name() = 'out']"/>
        </xsl:for-each>
        </xsl:copy>
    </xsl:template>
</xsl:stylesheet>
Obalix
@Obalix: As @Nick Jones point out, the OP wants this order: only @in, @in and @out, only @out. Also, you don't need the innermost `for-each`. You could use `fn:key()` as first step.
Alejandro
Note that the key `paramsByName` will apply to all `param` nodes of the document, to this solution may not work as is if the input document has multiple `paramList` which have `param`s with the same names.
dolmen
A: 

A small improvement:

In both @Nick-Jones ' and @Obalix 's solutions, it is shorter to write:

<xsl:copy-of select="current-group()/@*"/>

or

<xsl:copy-of select="key('paramsByName', @name)/@*"/>

than, respectively:

<xsl:for-each select="current-group()/@*">   
  <xsl:copy/>   
</xsl:for-each>   

or

<xsl:for-each select="key('paramsByName', @name)">     
 <xsl:copy-of select="@*"/>     
</xsl:for-each>     
Dimitre Novatchev
A: 

Obalix's answer may not work in the case there are multiple paramList elements in the input document. I assume this may interest the poster if the document describe a software interface where there are multiple procedures with a paramList for each.

Here is a sample input:

<root>
  <func name="one">
    <paramList>
      <param name="y" out="true"/>
      <param name="y" in="true"/>
      <param name="z" out="true"/>
      <param name="x" in="true"/>
    </paramList>
  </func>

  <func name="two">
    <paramList>
      <param name="z" in="true"/>
    </paramList>
  </func>
</root>

Here is my proposed stylesheet, built on Obalix's answer. The trick is to use a local key id containing the ID of the paramListelement.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
  <xsl:output method="xml" indent="yes"/>

  <xsl:key name="paramsByName" match="param" use="concat(generate-id(..), '/', @name)"/>
  <xsl:template match="paramList">
    <xsl:copy>
       <xsl:variable name="id" select="generate-id(.)"/>
       <xsl:for-each select="param[count(. | key('paramsByName', concat($id, '/', @name))[1]) = 1]">
         <xsl:copy>
           <xsl:copy-of select="key('paramsByName', concat($id, '/', @name))/@*"/>
         </xsl:copy>
       </xsl:for-each>
     </xsl:copy>
  </xsl:template>
</xsl:stylesheet>
dolmen
@dolmen: There is no such things as general solutions. With XSLT we bind an input with well known schema to an output with well known schema. So, if you are going to change the input schema, you will need a different stylesheet.
Alejandro
@Alejandro: I'm not proposing a general solution. The poster has a problem in a certain context. XML tags have meaning for the one that wrote the schema and I tried to understand how the XML fragment of the question may be used. A generic answer as the others proposed may bring some problems. Keys in XSLT are hard to understand and desserve explanations about their pitfalls.
dolmen
@dolmen: You wrote *"Obalix's answer may not work in the case there are multiple paramList elements"* Well... Not even the accepted answer will work with such change in the schema. That's what I've meant with *"There is no such things as general solutions"* statement. Mostly every change into input schema needs a change in the stylesheet.
Alejandro