tags:

views:

1744

answers:

3

Given the following xml fragment:

<Problems>
  <Problem>
    <File>file1</File>
    <Description>desc1</Description>
  </Problem>
  <Problem>
    <File>file1</File>
    <Description>desc2</Description>
  </Problem>
  <Problem>
    <File>file2</File>
    <Description>desc1</Description>
  </Problem>
</Problems>

I need to produce something like

<html>
  <body>
    <h1>file1</h1>
    <p>des1</p>
    <p>desc2</p>
    <h1>file2</h1>
    <p>des1</p>
  </body>
</html>

I tried using a key, like

<xsl:key name="files" match="Problem" use="File"/>

but I don't really understand how to take it to the next step, or if that's even the right approach.

+1  A: 

Here's how I'd do it, using the Muenchean method. Google 'xslt muenchean' for more info from smarter people. There might be a clever way, but I'll leave that to others.

One note, I avoid using capitals at the start of xml element names, eg 'File', but that's up to you.

<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:output method="html"/>
    <xsl:key name="files" match="/Problems/Problem/File" use="./text()"/>
    <xsl:template match="/">
     <html>
      <body>
       <xsl:apply-templates select="Problems"/>
      </body>
     </html>
    </xsl:template>
    <xsl:template match="Problems">
     <xsl:for-each select="Problem/File[generate-id(.) = generate-id(key('files', .))]">
      <xsl:sort select="."/>
      <h1>
       <xsl:value-of select="."/>
      </h1>
      <xsl:apply-templates select="../../Problem[File=current()/text()]"/>
     </xsl:for-each>
    </xsl:template>
    <xsl:template match="Problem">
     <p>
      <xsl:value-of select="Description/text()"/>
     </p>
    </xsl:template>
</xsl:stylesheet>

The idea is, key each File element using it's text value. Then only display the file values if they are the same element as the keyed one. To check if they're the same, use generate-id. There is a similar approach where you compare the first element that matches. I can't tell you which is more efficient.

I've tested the code here using Marrowsoft Xselerator, my favorite xslt tool, although no longer available, afaik. The result I got is:

<html>
<body>
<h1>file1</h1>
<p>desc1</p>
<p>desc2</p>
<h1>file2</h1>
<p>desc1</p>
</body>
</html>

This is using msxml4.

I have sorted the output by File. I'm not sure if you wanted that.

I hope this helps.

Richard A
Perfect, thanks! And cheers for the muenchean tip...
rjohnston
Cheers. I didn't even notice that you are a fellow Aussie, otherwise I would have used many more colloquialisms. :)
Richard A
@rjonston: I have provided a little bit cleaner and probably slightly more efficient solution than Richards.
Dimitre Novatchev
+2  A: 

This solution is a little bit simpler, more efficient and at the same time more general than the one presented by Richard:

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
<!--                                            -->
 <xsl:key name="kFileByVal" match="File"
       use="." />
<!--                                            -->
 <xsl:key name="kDescByFile" match="Description"
       use="../File"/>
<!--                                            -->
    <xsl:template match="/*">
     <html>
      <body>
      <xsl:for-each select=
         "*/File[generate-id()
                =
                 generate-id(key('kFileByVal',.)[1])]">
        <h1><xsl:value-of select="."/></h1>
        <xsl:for-each select="key('kDescByFile', .)">
          <p><xsl:value-of select="."/></p>
        </xsl:for-each>
      </xsl:for-each>
      </body>
     </html>
    </xsl:template>
</xsl:stylesheet>

when applied to the provided XML document:

<Problems>
    <Problem>
     <File>file1</File>
     <Description>desc1</Description>
    </Problem>
    <Problem>
     <File>file1</File>
     <Description>desc2</Description>
    </Problem>
    <Problem>
     <File>file2</File>
     <Description>desc1</Description>
    </Problem>
</Problems>

Produces the wanted result:

<html>
   <body>
      <h1>file1</h1>
      <p>desc1</p>
      <p>desc2</p>
      <h1>file2</h1>
      <p>desc1</p>
   </body>
</html>

Do note the simple match pattern of the first <xsl:key> and how, using a second <xsl:key>, we locate all "Description" elements that are siblings of a "File" element that has a given value.

We could have used more templates instead of <xsl:for-each> pull-processing, however this is a quite simple case and the solution really benefits from shorter, more compact and more readable code.

Also note, that in XSLT 2.0 one will typically use the <xsl:for-each-group> instruction instead of the Muenchian method.

Dimitre Novatchev
@Dimitre. I've only ever used keys for Muenchean grouping. Thanks for an example of using them in a more expansive way. Also, I've not yet looked at XSLT 2.0, so the for-each-group is interesting too. Cheers and Happy New Year.
Richard A
@Richard: Happy New Year to you, Richard.
Dimitre Novatchev
A: 

How would you go about selecting the unique Descriptions under the File nodes?

Thanks for the excellent posts!

M

Matt W
I had asked this after all this, please see my other question about sub groups of a group: http://stackoverflow.com/questions/2202774/xslt-grouping-and-subgrouping
James Campbell