views:

207

answers:

3

I guess my problem is quite common, so there must be an easy solution. Consider the following xml snippet:

  <categories>
    <category name="cat1" />
    <category name="cat2" />
    <category name="cat3" />
    <category name="cat4" />
  </categories>
  <data>
    <catval name="cat2">foo</catval>
    <catval name="cat4">bar</catval>
    <catval name="cat3">boo</catval>
  </data>

I need to output the catval values in the order defined in the categories element (including categories that have no data). Please note that in the real input xml, there are multiple data elements all over the place and the output is way more complex, so creating a template for categories is not feasible. I am using a construct like the following:

<xsl:template match="data">
    <xsl:variable name="currentdata" select="." />
    <xsl:for-each select="../categories/category">
      <xsl:value-of select="@name" />: 
      <xsl:value-of 
        select="$currentdata/catval[@name=@name]" /> <!-- ??? -->
    </xsl:for-each>
</xsl:template>

I don't know if this is the best approach to solve my problem, but even if it isn't: How can I match the name attribute of $currentdata/catval to the name attribute of the category element in the context of the for-each loop?

+1  A: 

Use a variable to save the attribute value from the <xsl:for-each> scope:

<xsl:template match="data">
  <xsl:variable name="currentdata" select="." />
  <xsl:for-each select="../categories/category">
    <xsl:variable name="name" select="@name"/>
    <xsl:value-of select="$name" />: 
    <xsl:value-of 
      select="$currentdata/catval[@name=$name]" /> <!-- ??? -->
  </xsl:for-each>
</xsl:template>
Roland Bouman
Didn't think of that, thank you. But is this really the only way do do it? To me it seems odd enough that I need a variable to refer to the scope outside the for-each loop...
FRotthowe
I believe you can use the `current()` function for that, so that you could write: `<xsl:value-of select="$currentdata/catval[@name=current()/@name]"/>`I admit that one always confuses the hell out of me, because most of the time `current()` is the same as `.` except when it's not :) So I stick to storing the value in a var.
Roland Bouman
One thing I'd like to point out: there is no checking to see of all catval items are actually visited, even if the order is not defined. I hope that's ok...
Roland Bouman
Only the defined categories should be shown, so this is correct for me.
FRotthowe
A: 

I would go the other way:

<xsl:template match="categories">
   <xsl:for-each select="category">
     <xsl:variable name="name" select="@name"/>
     <xsl:apply-template select="/data/catval[@name=$name]/>
   </xsl:for-each>
</xsl:template>

<xsl:template match="catval">
  <!-- Your output logic here -->
</xsl:template>

So, once you're in the "catval" template, you're sure the ordering is already done and you just have to focus on the output formatting.

gizmo
yeah this is nicer. I should've read more carefully. There is one thing though, that I didn't think of either...there is no checking to see of all catval items are actually visited, even if the order is not defined.
Roland Bouman
I would do this, but in the real xml there are multiple *data* elements all over the place but just one definition of the categories at the top.
FRotthowe
Ok, in that case, you migth also consider using the <key> element, to grab all <data> element in the document.
gizmo
+3  A: 

Simple, elegant and efficient:

<xsl:key name="catvalByName" match="catval" use="@name" />

<xsl:template match="category">
  <xsl:value-of select="@name" />
  <xsl:text>: </xsl:text>
  <xsl:value-of select="key('catvalByName', @name)" />
  <xsl:text>&#10;</xsl:text>
</xsl:template>

Would output:

cat1: 
cat2: foo
cat3: boo
cat4: bar

when called like this, for example:

<xsl:template match="categories">
  <xsl:apply-templates select="category" />
</xsl:template>
Tomalak