Question

PROBLEM:

I have a list of items in XML, and I want to display a table containing their names and attributes. This is fairly straightforward, except that the some of the items' properties are the sum of other items. In the following example, I want to display food name and total calories. Some of the items have a 'calories' attribute; others have a list of ingredients and the total calories should be the sum of the calories of each item in that list.

I'm ok with a solution that doesn't work for aggregate foods that contain other aggregate foods. I'm restricted to xsl 1.0.

<?xml version="1.0" encoding="ISO-8859-1"?>
<FoodList>
  <SimpleFood name="Banana" calories="50"/>
  <SimpleFood name="IceCream" calories="100"/>
  <AggregateFood name="BananaSplit">
    <IngredientList>
      <Ingredient typeRef="Banana"/>
      <Ingredient typeRef="IceCream"/>
      <Ingredient typeRef="IceCream"/>
    </IngredientList>
  </AggregateFood>
</FoodList>

Here's the basic stylesheet:

<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
  <html>
  <body>
    <xsl:variable name="foods" select="FoodList/*"/>
    <xsl:for-each select="$foods">
      <tr>
        <td>
          <xsl:value-of select="@name"/>
        </td>
        <td>
          <xsl:choose>
            <xsl:when test="local-name()='SimpleFood'">
              <xsl:value-of select="@calories"/>
            </xsl:when>
            <xsl:when test="local-name()='AggregateFood'">
              <!--                              -->
              <!-- YOUR AWESOME CODE GOES HERE! -->
              <!--                              -->
            </xsl:when>
          </xsl:choose>
        </td>
      </tr>
    </xsl:for-each>
  </body>
  </html>
</xsl:template>
</xsl:stylesheet>

Here's what I want to see as output:

<?xml version="1.0" encoding="UTF-8"?>
<html>
  <body>
    <tr>
      <td>Banana</td>
      <td>50</td>
    </tr>
    <tr>
      <td>IceCream</td>
      <td>100</td>
    </tr>
    <tr>
      <td>BananaSplit</td>
      <td>250</td>
    </tr>
  </body>
</html>

ATTEMPTS:

<xsl:value-of select="sum($foods/*[@name=current()/IngredientList/Ingredient/@typeRef]/@calories)"/>

Not only does this not work (returns 0 calories), but the idea of finding each element that matches an ingredient in the ingredient list is flawed, since it would single count the repeated ice cream ingredient and return 150 calories instead of the correct 250 (it would be cool if someone could show me a working version of this though, for the sake of personal curiosity).

Another attempt:

<xsl:variable name="ingredients">
  <xsl:for-each select="IngredientList/Ingredient">
    <xsl:element name="Ingredient">
      <xsl:attribute name="calories">
        <xsl:value-of select="$foods/x:*[@name=current()/@typeRef]"/>
      </xsl:attribute>
    </xsl:element>
  </xsl:for-each>
</xsl:variable>
<xsl:value-of select="sum($ingredients/@calories)"/>

The same thing with copy-element instead:

<xsl:variable name="ingredients">
  <xsl:for-each select="IngredientList/Ingredient">
    <xsl:copy-of select="$foods/x:*[@name=current()/@typeRef]"/>
  </xsl:for-each>
</xsl:variable>
<xsl:value-of select="sum($ingredients/@calories)"/>

Neither of the above work :( I wanted to post them as suggestions though to get the ball rolling; maybe you can find a simple error on my part.

I've formatted the example code so that you can pass it through online xsl transformers such as the one found here: http://www.freeformatter.com/xsl-transformer.html. I probably won't "accept" answers unless I can get my desired output using such a tool.

Thanks!

Was it helpful?

Solution

Using templates, a key and exsl:node-set I wrote

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exsl="http://exslt.org/common" exclude-result-prefixes="exsl">

<xsl:key name="by-name" match="SimpleFood" use="@name"/>

<xsl:template match="/">
  <html>
    <body>
      <xsl:apply-templates/>
    </body>
  </html>
</xsl:template>

<xsl:template match="FoodList">
  <table>
    <thead>
      <tr>
        <th>Name</th>
        <th>Calories</th>
      </tr>
    </thead>
    <tbody>
      <xsl:apply-templates/>
    </tbody>
  </table>
</xsl:template>

<xsl:template match="SimpleFood">
  <tr>
    <td><xsl:value-of select="@name"/></td>
    <td><xsl:value-of select="@calories"/></td>
  </tr>
</xsl:template>

<xsl:template match="AggregateFood">
  <tr>
    <td><xsl:value-of select="@name"/></td>
    <td>
      <xsl:variable name="ing-cals">
        <xsl:for-each select="IngredientList/Ingredient">
          <cal><xsl:value-of select="key('by-name', @typeRef)/@calories"/></cal>
        </xsl:for-each>
      </xsl:variable>
      <xsl:value-of select="sum(exsl:node-set($ing-cals)/cal)"/>
    </td>
  </tr>
</xsl:template>

</xsl:stylesheet>

which outputs

<html>
   <body>
      <table>
         <thead>
            <tr>
               <th>Name</th>
               <th>Calories</th>
            </tr>
         </thead>
         <tbody>

            <tr>
               <td>Banana</td>
               <td>50</td>
            </tr>

            <tr>
               <td>IceCream</td>
               <td>100</td>
            </tr>

            <tr>
               <td>BananaSplit</td>
               <td>250</td>
            </tr>

         </tbody>
      </table>
   </body>
</html>
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top