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.

Grundsätzlich geht es bei xsl:for-each-group darum, eine Sequenz von Items über einen gewählten Schlüssel (der im weiteren Programmverlauf durch current-grouping-key() ansprechbar ist) zu gruppieren (vgl. ). Das Ergebnis ist wiederum eine Sequenz von Items, die durch eine Schleife über die current-group() weiter ausgewertet werden kann.


<xsl:for-each-group 
     select="/Orte/Ort/Mensch/Kauf" 
     group-by="bez"/>

Möchten wir die Eingangssequenz weiter eingrenzen, können wir mit den bewährten Prädikaten arbeiten.


<xsl:for-each-group 
     select="/Orte/Ort/Mensch/Kauf
             [(anzahl * preis) &gt; 1000]" 
     group-by="bez"/>

Die vorstehende Eingangssequenz können wir alternativ auch so formulieren:


<xsl:for-each-group 
     select="for $x in /Orte/Ort/Mensch/Kauf 
             return 
             if ($x[(anzahl * preis) &gt; 1000]) 
             then $x 
             else ()" 
     group-by="bez"/>

Syntaktisch keine Verbesserung, aber didaktisch von Wert ist die Option über .


<xsl:for-each-group 
     select="/Orte/Ort/Mensch/Kauf 
             except 
             /Orte/Ort/Mensch/Kauf
                  [(anzahl * preis) &lt;= 1000]" 
     group-by="bez">

Unter XSLT 3.0 besteht auch die Möglichkeit, mit und einer selbst definierten Funktion zu arbeiten.


<xsl:for-each-group 
     select="for-each(/Orte/Ort/Mensch/Kauf, 
             function($p){ 
               $p[(anzahl * preis) &gt; 1000] 
             })" 
     group-by="bez"/>

Ebenfalls unter XSLT 3.0 können wir mit der -Funktion unser Ziel erreichen.


<xsl:for-each-group 
     select="filter (/Orte/Ort/Mensch/Kauf, 
              function($p) { 
                ($p/anzahl * $p/preis) &gt; 1000
              })" 
     group-by="bez">

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 / 19. März 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: info10@wilfried-grupe.de

www.wilfried-grupe.de/XSL_for_each_group.html