XSL - Übersicht / xsl:for-each-group

xsl:for-each-group

xsl:for-each-group

xsl:for-each-group ist in der Lage, eine durch XPath ausgewählte Itemlist durch einen gewählten Grupperungsschlüssel so zusammenzufassen, daß die in der Itemlist wiederholt aufgeführten Werte jeweils nur einmal auftauchen (etwa analog einer DISTINCT-Auswertung bei einer SQL-Datenbankabfrage). Anschließend kann die gruppierte Itemliste durch xsl:for-each select="current-group()" durchlaufen werden.

xsl:for-each-group

Leistungsfähig und hilfreich sind die über xsl:for-each-group, nicht zuletzt kombiniert mit mehreren Ausgabedokumenten, die via xsl:result-document generiert werden.


  <xsl:template name="gruppierungen">
  <Waren>
    <xsl:for-each-group 
         select="/Orte/Ort/Mensch/Kauf" 
         group-by="bez">
    <xsl:result-document 
      href="../output/{current-grouping-key()}.xml">
    <ERGEBNIS>
    <ware key="{ current-grouping-key() }">  
      <xsl:for-each select="current-group()">
      <Kauf>
        <Artikel>
          <xsl:value-of select="bez"/>
        </Artikel>
        <Anzahl>
          <xsl:value-of select="anzahl"/>
        </Anzahl>
        <Umsatz>
          <xsl:value-of select="Gesamt"/>
        </Umsatz>
        <VNKunde>
          <xsl:value-of select="../vorname"/>
        </VNKunde>
        <NNKunde>
          <xsl:value-of select="../name"/>
        </NNKunde>
        <WOKunde>
          <xsl:value-of select="../../name"/>
        </WOKunde>
      </Kauf>
      </xsl:for-each>
    </ware>    
    </ERGEBNIS>
    </xsl:result-document>
    </xsl:for-each-group>
  </Waren>
  </xsl:template>

Die Anweisung xsl:for-each-group ermöglicht es, eine klar definierte Datenmenge (im folgenden Beispiel "//Ort[3]/Mensch/Kauf") nach einem Schlüssel (hier: "bez", ist ein Childnode von "Kauf") zu gruppieren. Eventuell mehrfach auftretende Werte dieses Schlüssels (der current-grouping-key()) werden jeweils nur ein einziges Mal wiedergegeben; damit ist die Gruppierung der distinct-function eng verwandt.

pic/foreachgroup.png

Hinter jeder einzelnen Gruppierung (der current-group()) stehen aber noch gegebenenfalls mehrere Einzelwerte, die wir mit einer Schleife ansprechen können. Im dargestellten Beispiel habe ich nach dem Kauf-Element "bez" gruppiert, daher treten die im XML Input ursprünglich mehrfach auftretenden Einzelwerte "Hemd", "Hose", "Schuhe" im HTML Output nur ein einziges Mal auf. Um den Bezug vom XML Input zum HTML Output übersichtlich zu halten, habe ich drei Einzelfelder farblich markiert und mit Pfeilen verbunden.

Die XSL-Logik sieht so aus:


<xsl:template match="/">
  <html>
   <body>
    <table border="1">
     <xsl:for-each-group 
          select="//Ort[3]/Mensch/Kauf" 
          group-by="bez">
      <tr>
       <td valign="top">
        <xsl:value-of 
             select="current-grouping-key()" />
       </td>
       <td>
        <table>
         <xsl:for-each 
              select="current-group()">
          <xsl:sort 
               select="anzahl" 
               data-type="number" 
               order="ascending" />
          <tr>
           <td><xsl:value-of select="idMensch"/></td>
           <td><xsl:value-of select="anzahl"/></td>
           <td><xsl:value-of select="preis"/></td>
           <td><xsl:value-of select="Gesamt"/></td>
          </tr>
         </xsl:for-each>
        </table>
       </td>
      </tr>
     </xsl:for-each-group>
    </table>
   </body>
  </html>
</xsl:template>

Zusammengesetzte Schlüssel

Daneben besteht die Möglichkeit, über die "concat"-Funktion zusammengesetzte Schlüssel zu erstellen. In der ausgewählten Nodeliste "//Ort/Mensch/Kauf" haben wir mit group-by="concat(bez, ' ', ../../name)" einen Schlüssel generiert, der seine Informationen aus dem "Kauf"-Childnode "bez" sowie aus dem "Ort/name" (ausgehend von "Kauf" erreichbar durch "../../name") bezieht. Auf diese Welse kommt jede relevante Kombination aus Ortname und Artikelname zustande, die wir mit <xsl:value-of select="current-grouping-key()" /> darstellen können.

pic/foreachgroup_schluessel2.jpg

Innerhalb dieser jeweiligen Gruppierung (<xsl:for-each select="current-group()">, dessen aktueller Node immer noch "Kauf" ist) können wir nun noch di Einzelinformationen ermitteln, etwa mit <xsl:value-of select="../vorname" />, das sich auf den "Kauf"-Parentnode "Mensch" bezieht.


 <xsl:template match="/">
  <html>
   <body>
    <table border="1">
     <xsl:for-each-group 
     select="//Ort/Mensch/Kauf"
     group-by="concat(bez, ' ', ../../name)">
      <tr>
       <td valign="top">
        <xsl:value-of 
             select="current-grouping-key()" />
       </td>
       <td>
        <table>
         <xsl:for-each 
              select="current-group()">
          <xsl:sort select="anzahl" 
               data-type="number" 
               order="ascending" />
          <tr>
           <td>
            <xsl:value-of select="../vorname" />
           </td>
           <td>
            <xsl:value-of select="../name" />
           </td>
           <td>
            <xsl:value-of select="anzahl" />
           </td>
          </tr>
         </xsl:for-each>
        </table>
       </td>
      </tr>
     </xsl:for-each-group>
    </table>
   </body>
  </html>
 </xsl:template>

Gruppieren mit group-adjacent

Im Unterschied zu "xsl:for-each-group" mit "group-by" öffnet "group-adjacent" jedes Mal eine neue Gruppe, wenn sich der Wert des in "group-adjacent" bezeichneten Feldes ändert. Da sich die ursprüngliche Reihenfolge nicht ändert, kann derselbe Schlüssel mehrfach auftreten.

pic/group_adjacent.jpg

Damit entsteht aus folgender Logik ...


<root>
  <xsl:for-each-group 
       select="//Ort[1]/Mensch/Kauf" 
       group-adjacent="bez">
    <key name="{bez}">
      <xsl:for-each select="current-group()">
        <wert name="{bez}" />          
      </xsl:for-each>
    </key>
  </xsl:for-each-group>
</root>

... dieses Ergebnis:


<root>
  <key name="Hemd">
    <wert name="Hemd"/>
    <wert name="Hemd"/>
    <wert name="Hemd"/>
  </key>
  <key name="Hose">
    <wert name="Hose"/>
    <wert name="Hose"/>
    <wert name="Hose"/>
    <wert name="Hose"/>
  </key>
  <key name="Schuhe">
    <wert name="Schuhe"/>
    <wert name="Schuhe"/>
  </key>
  <key name="Hose">
    <wert name="Hose"/>
    <wert name="Hose"/>
    <wert name="Hose"/>
    <wert name="Hose"/>
    <wert name="Hose"/>
    <wert name="Hose"/>
    <wert name="Hose"/>
  </key>
  <key name="Hemd">
    <wert name="Hemd"/>
    <wert name="Hemd"/>
    <wert name="Hemd"/>
  </key>
  <key name="Hose">
    <wert name="Hose"/>
    <wert name="Hose"/>
    <wert name="Hose"/>
  </key>
  <key name="Hemd">
    <wert name="Hemd"/>
  </key>
  <key name="Hose">
    <wert name="Hose"/>
    <wert name="Hose"/>
    <wert name="Hose"/>
  </key>
  <key name="Schuhe">
    <wert name="Schuhe"/>
    <wert name="Schuhe"/>
  </key>
  <key name="Hemd">
    <wert name="Hemd"/>
    <wert name="Hemd"/>
    <wert name="Hemd"/>
  </key>
  <key name="Hose">
    <wert name="Hose"/>
    <wert name="Hose"/>
  </key>
  <key name="Schuhe">
    <wert name="Schuhe"/>
    <wert name="Schuhe"/>
    <wert name="Schuhe"/>
    <wert name="Schuhe"/>
  </key>
  <key name="Hemd">
    <wert name="Hemd"/>
    <wert name="Hemd"/>
  </key>
  <key name="Hose">
    <wert name="Hose"/>
  </key>
</root>

Gruppieren mit group-starting-with und group-ending-with

Die Arbeit mit group-starting-with und group-ending-with betrifft häufig, mäßig strukturierten XML-Dokumente nachträglich eine übersichtlichere Struktur zu verleihen. Dabei wird jedesmal eine neue Gruppe gestartet, wenn das in group-starting-with definierte Start-Muster zutrifft; eine Gruppe wird geschlossen, wenn das in group-ending-with definierte End-Muster zutrifft.

Was einfach klingen mag, kann in der Realität etwas komplexer werden. Das möchte ich in folgendem Beispiel erläutern. Gegeben sei das folgende XML Input Dokument, bei dem sich eine Struktur lediglich aus der Reihenfolge der Elemente ergibt. Zum Beispiel beginnt eine Kauf-Gruppe mit "idMensch", sie endet mit "Gesamt". Eine "Mensch"-Gruppe beginnt mit "id", wenn das übernächste Element "vorname" heißt, sonst ist es eine "Ort"-Gruppe.


<?xml version="1.0" standalone="yes"?>
<ROOT_SEQ>
  <id>1</id>
  <name>Neustadt</name>
  <id>1</id>
  <name>Holzflos</name>
  <vorname>Hugo</vorname>
  <Gehalt>234.56</Gehalt>
  <idOrt>1</idOrt>
  <idMensch>1</idMensch>
  <anzahl>3</anzahl>
  <bez>Hemd</bez>
  <preis>12.99</preis>
  <Gesamt>38.97</Gesamt>
  <idMensch>1</idMensch>
  <anzahl>9</anzahl>
  <bez>Hemd</bez>
  <preis>12.99</preis>
  <Gesamt>116.91</Gesamt>
  <idMensch>1</idMensch>
  <anzahl>8</anzahl>
  <bez>Hemd</bez>
  <preis>12.99</preis>
  <Gesamt>103.92</Gesamt>
  <idMensch>1</idMensch>
  <anzahl>9</anzahl>
  <bez>Hose</bez>
  <preis>25.99</preis>
  <Gesamt>233.91</Gesamt>
  <idMensch>1</idMensch>
  <anzahl>9</anzahl>
  <bez>Hose</bez>
  <preis>25.99</preis>
  <Gesamt>233.91</Gesamt>
  <idMensch>1</idMensch>
  <anzahl>8</anzahl>
  <bez>Hose</bez>
  <preis>25.99</preis>
  <Gesamt>207.92</Gesamt>
  <idMensch>1</idMensch>
  <anzahl>8</anzahl>
  <bez>Hose</bez>
  <preis>25.99</preis>
  <Gesamt>207.92</Gesamt>
  <idMensch>1</idMensch>
  <anzahl>8</anzahl>
  <bez>Schuhe</bez>
  <preis>151.23</preis>
  <Gesamt>1209.84</Gesamt>
  <idMensch>1</idMensch>
  <anzahl>8</anzahl>
  <bez>Schuhe</bez>
  <preis>151.23</preis>
  <Gesamt>1209.84</Gesamt>
  <id>4</id>
  <name>Nixlos</name>
  <vorname>Nicole</vorname>
  <Gehalt>1234.56</Gehalt>
  <idOrt>1</idOrt>
  <idMensch>4</idMensch>
  <anzahl>8</anzahl>
  <bez>Hose</bez>
  <preis>25.99</preis>
  <Gesamt>207.92</Gesamt>
  <idMensch>4</idMensch>
  <anzahl>7</anzahl>
  <bez>Hose</bez>
  <preis>25.99</preis>
  <Gesamt>181.92999999999998</Gesamt>
  <idMensch>4</idMensch>
  <anzahl>6</anzahl>
  <bez>Hose</bez>
  <preis>25.99</preis>
  <Gesamt>155.94</Gesamt>
  <idMensch>4</idMensch>
  <anzahl>5</anzahl>
  <bez>Hose</bez>
  <preis>25.99</preis>
  <Gesamt>129.95</Gesamt>
  <idMensch>4</idMensch>
  <anzahl>4</anzahl>
  <bez>Hose</bez>
  <preis>25.99</preis>
  <Gesamt>103.96</Gesamt>
  <idMensch>4</idMensch>
  <anzahl>4</anzahl>
  <bez>Hose</bez>
  <preis>25.99</preis>
  <Gesamt>103.96</Gesamt>
  <idMensch>4</idMensch>
  <anzahl>3</anzahl>
  <bez>Hose</bez>
  <preis>25.99</preis>
  <Gesamt>77.97</Gesamt>
  <id>9</id>
  <name>Sprachlos</name>
  <vorname>Stefan</vorname>
  <Gehalt>5430</Gehalt>
  <idOrt>1</idOrt>
  <idMensch>9</idMensch>
  <anzahl>11</anzahl>
  <bez>Hemd</bez>
  <preis>12.99</preis>
  <Gesamt>142.89000000000002</Gesamt>
  <idMensch>9</idMensch>
  <anzahl>22</anzahl>
  <bez>Hemd</bez>
  <preis>12.99</preis>
  <Gesamt>285.78000000000003</Gesamt>
</ROOT_SEQ>

Wie Sie sehen, wird für jedes "id"-Element im Input ein Ort-Element im Output gebildet, sofern das übernächste Element ebenfalls "id" heißt. Daher wird hier auch mit "following-sibling" gearbeitet.

Nun startet innerhalb der aktuellen Gruppierung eine weitere, sie fragt, ob das schließende Element "idOrt" heißt. Damit wird die Gruppe geschlossen, das folgende Element ist dann schon "idMensch". Um von hier aus an die Informationen der soeben geschlossenen Gruppe heranzukommen, arbeiten wir mit "preceding-sibling".


<root>
<xsl:for-each-group 
 select="/ROOT_SEQ/child::*"
 group-starting-with="id[following-sibling::*[position()=2]/local-name()='id']">
 <xsl:variable name="vidort" select="." />
 <Ort name="{following-sibling::*[position()=1]}" id="{$vidort}">
  <xsl:for-each-group select="current-group()"
   group-ending-with="idOrt">
   <xsl:if test="preceding-sibling::*[position()=1] = $vidort">
    <xsl:variable name="vidMensch"
     select="preceding-sibling::*[position()=5]" />
    <Mensch locname="{local-name()}" 
            idort="{preceding-sibling::*[position()=1]}"
            id="{$vidMensch}" 
            vorname="{preceding-sibling::*[position()=3]}"
            name="{preceding-sibling::*[position()=4]}">
    </Mensch>
   </xsl:if>
  </xsl:for-each-group>
 </Ort>
</xsl:for-each-group>
</root>

Zur besseren Orientierung und zum Verständnis des Ergebnisses habe ich die Anweisung "locname='{local-name()}'" eingearbeitet. Das Ergebnis ist überschaubar:


   <root>
      <Ort name="Neustadt" id="1">
         <Mensch locname="idMensch"
                 idort="1"
                 id="1"
                 vorname="Hugo"
                 name="Holzflos"/>
         <Mensch locname="idMensch"
                 idort="1"
                 id="4"
                 vorname="Nicole"
                 name="Nixlos"/>
         <Mensch locname="idMensch"
                 idort="1"
                 id="9"
                 vorname="Stefan"
                 name="Sprachlos"/>
      </Ort>
   </root>

Wenn Sie die gesamte Logik haben möchten, die auch die Kauf-Elemente samt Attributen anzeigt, bitte sehr:


<root>
<xsl:for-each-group 
 select="/ROOT_SEQ/child::*"
 group-starting-with="id[following-sibling::*[position()=2]/local-name()='id']">
 <xsl:variable name="vidort" select="." />
 <Ort name="{following-sibling::*[position()=1]}" 
      id="{$vidort}">
  <xsl:for-each-group select="current-group()"
   group-ending-with="idOrt">
   <xsl:if test="preceding-sibling::*[position()=1] = $vidort">
    <xsl:variable name="vidMensch"
     select="preceding-sibling::*[position()=5]" />
    <Mensch locname="{local-name()}" 
            idort="{preceding-sibling::*[position()=1]}"
            id="{$vidMensch}" 
            vorname="{preceding-sibling::*[position()=3]}"
            name="{preceding-sibling::*[position()=4]}">
     <xsl:for-each-group 
          select="current-group()"
          group-ending-with="Gesamt">
      <xsl:if test="./text() = $vidMensch">
       <Kauf locname="{local-name()}" idmensch="{.}">
        <xsl:for-each select="current-group()">
         <xsl:attribute name="{local-name()}">
          <xsl:value-of select="." />
         </xsl:attribute>
        </xsl:for-each>
       </Kauf>
      </xsl:if>
     </xsl:for-each-group>
    </Mensch>
   </xsl:if>
  </xsl:for-each-group>
 </Ort>
</xsl:for-each-group>
</root>

wg / 14. Januar 2018



Fragen? Anmerkungen? Tips?

Bitte nehmen Sie Kontakt zu mir auf:

Vorname
Nachname
Mailadresse







Vielen Dank für Ihr Interesse an meiner Arbeit.


V.i.S.d.P.: Wilfried Grupe * Klus 6 * 37643 Negenborn

☎ 0151. 750 360 61 * eMail: info2018@wilfried-grupe.de

www.wilfried-grupe.de/XSL_for_each_group.html