tags:

views:

470

answers:

3

(Note: I have posted a variation on my earlier question as suggested)

Given an input xml file with following structure:

  <widgets>
    <widget shape="square" material="wood" color="red" />
    <widget shape="square" material="metal" color="blue" />
    <widget shape="square" material="plastic" color="green" />
    <widget shape="square" material="kevlar" color="red" />
    <widget shape="round" material="metal" color="orange" />
    <widget shape="round" material="wood" color="green" />
    <widget shape="round" material="kevlar" color="blue" />
    <widget shape="diamond" material="plastic" color="blue" />
    <widget shape="diamond" material="wood" color="brown" />
    <widget shape="diamond" material="metal" color="red" />
  </widgets>

And the following information:

  1. Each widget has a shape, material and color
  2. Each shape, material and color combination is unique
  3. Not every combination of shape, material and color exists eg there is no round, plastic widget
  4. There can be unlimited shapes, materials and colors
  5. The desired output is a table of where each row represents a shape and each column represents a material.

How can I output the following structure using XSLT?

  <table>
    <tr id="diamond">
      <td class="kevlar"></td>
      <td class="metal red"></td>
      <td class="plastic blue"></td>
      <td class="wood brown"></td>
    </tr>
    <tr id="round">
      <td class="kevlar blue"></td>
      <td class="metal orange"></td>
      <td class="plastic"></td>
      <td class="wood green"></td>
    </tr>
    <tr id="square">
      <td class="kevlar green"></td>
      <td class="metal blue"></td>
      <td class="plastic green"></td>
      <td class="wood red"></td>
    </tr>
  </table>
+1  A: 
Tomalak
+1  A: 

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
 <xsl:output omit-xml-declaration="yes" indent="yes"/>

 <xsl:key name="kShapeByVal" match="@shape"
  use="."/>

 <xsl:key name="kMaterByVal" match="@material"
  use="."/>

 <xsl:key name="kcolorByVal" match="@color"
  use="."/>

 <xsl:key name="kColorByShapeAndMat" match="@color"
  use="concat(../@shape, '+', ../@material)"/>

  <xsl:variable name="vShapes" select=
  "/*/*/@shape
          [generate-id()
          =
           generate-id(key('kShapeByVal',.)[1])
           ]
  "/>

  <xsl:variable name="vMaterials" select=
  "/*/*/@material
          [generate-id()
          =
           generate-id(key('kMaterByVal',.)[1])
           ]
  "/>

  <xsl:variable name="vColors" select=
  "/*/*/@color
          [generate-id()
          =
           generate-id(key('kcolorByVal',.)[1])
           ]
  "/>

    <xsl:template match="/*">
      <table>
         <xsl:for-each select="$vShapes">
           <xsl:sort select="."/>

           <xsl:variable name="vShape" select="."/>

           <tr id="{.}">
             <xsl:for-each select="$vMaterials">
               <xsl:sort select="."/>

               <xsl:variable name="vMat" select="."/>

               <xsl:variable name="vShapeMatColors" select=
               "key('kColorByShapeAndMat',
                    concat($vShape, '+', $vMat)
                   )
                "/>

                <xsl:if test="not($vShapeMatColors)">
                  <td class="{$vMat}"></td>
                </xsl:if>

                <xsl:for-each select="$vShapeMatColors">
                  <td class="{concat($vMat, ' ', .)}"></td>
                </xsl:for-each>

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

</xsl:stylesheet>

when applied on the provided XML document:

<widgets>
    <widget shape="square" material="wood" color="red" />
    <widget shape="square" material="metal" color="blue" />
    <widget shape="square" material="plastic" color="green" />
    <widget shape="square" material="kevlar" color="red" />
    <widget shape="round" material="metal" color="orange" />
    <widget shape="round" material="wood" color="green" />
    <widget shape="round" material="kevlar" color="blue" />
    <widget shape="diamond" material="plastic" color="blue" />
    <widget shape="diamond" material="wood" color="brown" />
    <widget shape="diamond" material="metal" color="red" />
</widgets>

produces the wanted result:

<table>
   <tr id="diamond">
      <td class="kevlar"/>
      <td class="metal red"/>
      <td class="plastic blue"/>
      <td class="wood brown"/>
   </tr>
   <tr id="round">
      <td class="kevlar blue"/>
      <td class="metal orange"/>
      <td class="plastic"/>
      <td class="wood green"/>
   </tr>
   <tr id="square">
      <td class="kevlar red"/>
      <td class="metal blue"/>
      <td class="plastic green"/>
      <td class="wood red"/>
   </tr>
</table>

How it all works:

  1. Using the Muenchian method for grouping we find all different shapes, materials and colors -- in the variables $vShapes, $vMaterials and $vColors.

  2. We output a <tr> for every value in $vShapes

  3. For all possible materials as contained in $vMaterials we output one or more <td> elements with attribute class the value of which is determined in two separate cases:

  4. The first case is when there is no color specified for this combination of shape and material (key('kColorByShapeAndMat', concat($vShape, '+', $vMat) is empty). In this case tha class attribute contains just the material.

  5. The second case is when there is one or more colors specified for this combination of shape and material. Then, for every such color, a separate <td> element is output and its class attribute is produced as the concatenation of the material and the color, separated by a space.

Dimitre Novatchev
Thanks Dimitre - questions: 1) Am I right that kcolorByVal and vColors are not actually used in the transformation? 2) Is your solution significantly different from Tomalak's (except for the pattern matching approach)? 3) Would an XSLT 2.0 solution be any faster for large data sets?
eft
@eft Yes, kcolorByVal and vColors are not needed, remained in the process of refining the solution. As for an XSLT 2.0 solution, it depends on the optimization capabilities of the particular XSLT 2.0 processor being used. Would you like me to provide an XSLT 2.0 solution in a separate answer?
Dimitre Novatchev
@eft as for Tomalak's answer, I think the two answers are similar. However, I personally have difficulties understanding Tomalak's code -- for example he has a variable named $vShapes that contains ... "widget" elements.
Dimitre Novatchev
Dimitre - I have Saxon 8 and an XSLT 2.0 solution would be fantastic. Thank you.
eft
A: 

As asked in a coment by eft, here is an XSLT 2.0 solution:

<xsl:stylesheet version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="xs"
    >
    <xsl:output omit-xml-declaration="yes" indent="yes"/>

    <xsl:key name="kColorByShapeAndMat" match="@color"
     use="concat(../@shape, '+', ../@material)"/>

    <xsl:template match="/*">
      <xsl:for-each-group select="*/@shape" group-by=".">
        <xsl:sort select="."/>

        <xsl:variable name="vShape" select="current-grouping-key()"/>
        <tr id="{.}">
          <xsl:for-each-group select="/*/*/@material" group-by=".">
            <xsl:sort select="."/>

              <xsl:variable name="vMat" select="."/>

              <xsl:variable name="vColors" 
               select="key('kColorByShapeAndMat',
                            concat($vShape,'+',.)
                         )"/>
            <xsl:for-each select="''[empty($vColors)],$vColors/concat(' ',.)">
             <xsl:sort select="."/>

             <td class="{concat($vMat,.)}"></td>
            </xsl:for-each>
          </xsl:for-each-group>
        </tr>
      </xsl:for-each-group>
    </xsl:template>
</xsl:stylesheet>

when this transformation is applied on the originally provided XML document:

<widgets>
    <widget shape="square" material="wood" color="red" />
    <widget shape="square" material="metal" color="blue" />
    <widget shape="square" material="plastic" color="green" />
    <widget shape="square" material="kevlar" color="red" />
    <widget shape="round" material="metal" color="orange" />
    <widget shape="round" material="wood" color="green" />
    <widget shape="round" material="kevlar" color="blue" />
    <widget shape="diamond" material="plastic" color="blue" />
    <widget shape="diamond" material="wood" color="brown" />
    <widget shape="diamond" material="metal" color="red" />
</widgets>

the required result is produced:

<tr id="diamond">
   <td class="kevlar"/>
   <td class="metal red"/>
   <td class="plastic blue"/>
   <td class="wood brown"/>
</tr>
<tr id="round">
   <td class="kevlar blue"/>
   <td class="metal orange"/>
   <td class="plastic"/>
   <td class="wood green"/>
</tr>
<tr id="square">
   <td class="kevlar red"/>
   <td class="metal blue"/>
   <td class="plastic green"/>
   <td class="wood red"/>
</tr>
Dimitre Novatchev
Dimitre - Thank you very much - I will try it out
eft