tags:

views:

274

answers:

1

I have this XML:

<?xml version="1.0"?>
<?xml-stylesheet type="text/xsl" href="xsl.xsl"?>
<Response>
    <Result>
        <Date Unix="1263340800">13 Jan 2010 00:00:00</Date>
        <Column>
            <Name>Small</Name>
            <Value>100</Value>
        </Column>
    </Result>
    <Result>
        <Date Unix="1263427200">14 Jan 2010 00:00:00</Date>
        <Column>
            <Name>Small</Name>
            <Value>100</Value>
        </Column>
    </Result>
    <Result>
        <Date Unix="1263485232">14 Jan 2010 16:07:12</Date>
        <Column>
            <Name>Normal</Name>
            <Value>36.170537</Value>
        </Column>
    </Result>
    <Result>
        <Date Unix="1263513600">15 Jan 2010 00:00:00</Date>
        <Column>
            <Name>Small</Name>
            <Value>100</Value>
        </Column>
    </Result>
</Response>

I want to present the data in this way:

<table>
    <tr>
        <th>Time</th>
        <th>Small</th>
        <th>Normal</th>
    </tr>
    <tr>
        <td>13 Jan 2010 00:00:00</td>
        <td class="class_Small">100</td>
        <td class="class_Normal"></td>
    </tr>
    <tr>
        <td>14 Jan 2010 00:00:00</td>
        <td class="class_Small">100</td>
        <td class="class_Normal"></td>
    </tr>
    <tr>
        <td>14 Jan 2010 16:07:12</td>
        <td class="class_Small"></td>
        <td class="class_Normal">39.301737</td>
    </tr>
    <tr>
        <td>15 Jan 2010 00:00:00</td>
        <td class="class_Small">100</td>
        <td class="class_Normal"></td>
    </tr>
</table>

This is my current XSL:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt; 

   <xsl:key name="Friendlies" match="Name" use="."/> 
   <xsl:template match="/"> 

    <table> 
    <tr> 
        <th>Time</th> 
        <!-- Output the Column Names --> 
        <xsl:for-each select="//Column"> 

            <!-- Only ouput the Name if it is the first occurence of this value --> 
            <xsl:if test="generate-id(Name) = generate-id(key('Friendlies',Name)[1])">             

            <th> 
            <xsl:value-of select="Name"/> 
            </th>

            </xsl:if>                
        </xsl:for-each>
         </tr>
    <!-- Loop through all Results --> 
    <xsl:for-each select="//Result"> 
        <xsl:variable name="UnixTimestamp" select="Date/@Unix"/>
        <tr> 
            <td> 
                <xsl:value-of select="Date"/>
            </td> 

            <xsl:for-each select="//Column">
                <xsl:if test="generate-id(Name) = generate-id(key('Friendlies',Name)[1])">      

                <xsl:element name="td">
                    <xsl:attribute name="class">class_<xsl:value-of select="Name"/></xsl:attribute>
                    <xsl:value-of select="[Date/@Unix=$UnixTimestamp]/Value"/>
                </xsl:element>

                </xsl:if>
            </xsl:for-each> 

               </tr>
    </xsl:for-each> 
    </table>
   </xsl:template> 
</xsl:stylesheet> 

The HTML structure is coming out ok. The only problem is that the values aren't coming out in the cells. I realise that <xsl:value-of select="[Date/@Unix=$UnixTimestamp]/Value"/> is not a correct reference, because the Date element is actually at the same level as the Column element, and not within it. I can't think of how this should be done though.

Also, I'm not really sure if I'm using the best method for this because it seems like there would be an awful lot of looping happening for little output. As the XML gets bigger (much more Results, more Columns), I wonder whether there would be a performance impact.

Forgive me, I'm a bit of a newbie to XSL.

Thanks in advance.


Added after Tomalak's response

Some info about the XML:

  • Any number of <Column> nodes within each <Result> node
  • Only one <Date> node within each <Result> node
  • Only one <Name> node within each <Column> node
  • The value of the <Name> node can be anything, not just 'Small' or 'Normal'
+4  A: 

You are over-complicating matters. How about:

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

  <xsl:template match="Response">
    <table>
      <tr>
        <th>Time</th>
        <th>Small</th>
        <th>Normal</th>
      </tr>
      <xsl:apply-templates select="Result" />
    </table>
  </xsl:template>

  <xsl:template match="Result">
    <tr>
      <td><xsl:value-of select="Date" /></td>
      <td><xsl:value-of select="Column[Name = 'Small']/Value" /></td>
      <td><xsl:value-of select="Column[Name = 'Normal']/Value" /></td>
    </tr>
  </xsl:template>

</xsl:stylesheet>

The output is exactly as you require:

<table>
  <tr>
    <th>Time</th>
    <th>Small</th>
    <th>Normal</th>
  </tr>
  <tr>
    <td>13 Jan 2010 00:00:00</td>
    <td>100</td>
    <td></td>
  </tr>
  <tr>
    <td>14 Jan 2010 00:00:00</td>
    <td>100</td>
    <td></td>
  </tr>
  <tr>
    <td>14 Jan 2010 16:07:12</td>
    <td></td>
    <td>36.170537</td>
  </tr>
  <tr>
    <td>15 Jan 2010 00:00:00</td>
    <td>100</td>
    <td></td>
  </tr>
</table>

Your XSLT is so complicated mainly because of these:

<xsl:for-each select="//Column"> 

Two tips in this regard:

  • Don't do for-each as long as you can avoid it. 98% of the time you want apply-templates instead (even if it is somewhat hard to wrap your head around it at first). Resist the urge to use for-each, it bloats your code.
  • Don't use the // operator as long as you can avoid it. Your input XML is perfectly structured, using // like you do makes this structure unusable by completely flattening it. Instead, try to use the existing XML structure to your advantage, like I've done.

EDIT

Following up a comment from the OP. The solution below copes with any number and kind of <Name> nodes. It uses for-each, but as a means of abstract iteration, not as a means of processing child nodes.

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

  <xsl:key name="kName" match="Name" use="." />

  <xsl:variable name="vName" select="
    //Name[generate-id() = generate-id(key('kName', .)[1])]
  " />

  <xsl:template match="Response">
    <table>
      <tr>
        <th>Time</th>
        <xsl:for-each select="$vName">
          <th><xsl:value-of select="." /></th>
        </xsl:for-each>
      </tr>
      <xsl:apply-templates select="Result" />
    </table>
  </xsl:template>

  <xsl:template match="Result">
    <xsl:variable name="self" select="." />
    <tr>
      <td><xsl:value-of select="Date" /></td>
      <xsl:for-each select="$vName">
        <td><xsl:value-of select="$self/Column[Name = current()]/Value" /></td>
      </xsl:for-each>
    </tr>
  </xsl:template>

</xsl:stylesheet>

You could use <xsl:sort> to modify the order in which columns are generated.

For maximum performance with large input documents, another tweak could speed up this expression:

<xsl:value-of select="$self/Column[Name = current()]/Value" />

With this additional key:

<xsl:key name="kColumn" match="Column" use="concat(Name,'|',generate-id(..))" />

you could re-write it as:

<xsl:value-of select="key('kColumn', concat(.,'|',generate-id($self)))/Value" />
Tomalak
Thanks for your response. I probably didn't provide enough information about the XML.Some info about the XML:- Any number of <Column> nodes within each <Result> node- Only one <Date> node within each <Result> node- Only one <Name> node within each <Column> node- The value of the <Name> node can be anything, not just 'Small' or 'Normal'
Baraka
So you want to group on all available `<Name>` nodes?
Tomalak
I will try it out properly tomorrow but from going through the logic of your edited response, I think this is exactly what I was after. Will let you know. Thanks
Baraka
The solution in your Edit worked a treat and makes total sense. Thanks
Baraka