eft
eft

Reputation: 2601

XSLT to denormalize/pivot/flatten xml file? Part 2

(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>

Upvotes: 3

Views: 1393

Answers (3)

Tomalak
Tomalak

Reputation: 338158

A variant of my answer to your part 1 of the question does it:

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

  <!-- prepare some keys for later use -->
  <xsl:key name="kWidgetsByShape"       match="widget" use="@shape" />
  <xsl:key name="kWidgetsByMaterial"    match="widget" use="@material" />
  <xsl:key name="kWidgetsByComposition" match="widget" use="concat(@shape, ',', @material)" />

  <!-- select the <widget>s that are the first in their respective @shape -->
  <xsl:variable name="vShapes" select="
    /widgets/widget[
      generate-id()
      =
      generate-id(key('kWidgetsByShape', @shape)[1])
    ]
  " />  

  <!-- select the <widget>s that are the first in their respective @material -->
  <xsl:variable name="vMaterials" select="
    /widgets/widget[
      generate-id()
      =
      generate-id(key('kWidgetsByMaterial', @material)[1])
    ]
  " />

  <!-- output basic table structure -->
  <xsl:template match="/widgets">
    <table title="shapes: {count($vShapes)}, materials: {count($vMaterials)}">
      <xsl:apply-templates select="$vShapes" mode="tr">
        <xsl:sort select="@shape" />
      </xsl:apply-templates>
    </table>
  </xsl:template>

  <!-- output the <tr>s, one for each @shape -->
  <xsl:template match="widget" mode="tr">
    <tr id="{@shape}">
      <xsl:apply-templates select="$vMaterials" mode="td">
        <xsl:sort select="@material" />
        <xsl:with-param name="vCurrentShape" select="@shape" />
      </xsl:apply-templates>
    </tr>
  </xsl:template>

  <!-- output the right number of <td>s in each row, empty or not -->
  <xsl:template match="widget" mode="td">
    <xsl:param name="vCurrentShape" />

    <xsl:variable 
      name="vWidget" 
      select="key('kWidgetsByComposition', concat($vCurrentShape, ',', @material))[1]" 
    />

    <td class="{normalize-space(concat(@material, ' ', $vWidget/@color))}">
      <xsl:apply-templates select="$vWidget" />
    </td>
  </xsl:template>

  <xsl:template match="widget">
    <xsl:value-of select="." />
  </xsl:template>

</xsl:stylesheet>

Which produces:

<table title="shapes: 3, materials: 4">
  <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 red"></td>
    <td class="metal blue"></td>
    <td class="plastic green"></td>
    <td class="wood red"></td>
  </tr>
</table>

Basically all the things I said in my other answer still apply.

This time I used three <xsl:key>s instead of two. Two of them are used for iteration, and one for looking up <widget>s by @shape and @material.

I use different template modes in conjunction with <xsl:apply-templates> instead of <xsl:for-each>. This makes the code a few lines longer, but it is beneficial to clarity and readability.

The last template (<xsl:template match="widget">) is there for demonstrative purposes only, showing you how you could go on. It is called from within <xsl:template match="widget" mode="td">, once for each <widget> that actually exists.

Upvotes: 2

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243469

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>

Upvotes: 1

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243469

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <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.

Upvotes: 3

Related Questions