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

(Note: I posted a version of my previous question as suggested)

Given the input xml file with the 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:

  • Each widget has a shape, material and color.
  • Each combination of shape, material and color is unique.
  • Not every combination of shape, material and color exists, for example, there is no round plastic widget
  • There may be unlimited shapes, materials and colors.
  • The desired result is a table in which each row represents a form, and each column represents a material.

How can I derive 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>
+3
source share
3 answers

:

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

XML-:

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

:

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

:

  • Muenchian , , - $vShapes, $vMaterials $vColors.

  • <tr> $vShapes

  • , $vMaterials, <td> class, :

  • , , (key('kColorByShapeAndMat', concat($vShape, '+', $vMat) ). class .

  • - . <td>, class , .

+3

1 ::

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

:

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

, , - .

<xsl:key> . <widget> @shape @material.

<xsl:apply-templates> <xsl:for-each>. , .

(<xsl:template match="widget">) , , . <xsl:template match="widget" mode="td">, <widget>, .

+2

As already mentioned in coment eft, here is the 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 conversion is applied to 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 created :

<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>
+1
source

Source: https://habr.com/ru/post/1706841/


All Articles