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, dass 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. XQuery). 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 Sie die Eingangssequenz weiter eingrenzen, können Sie 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 Sie 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 Sie 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 -Funktion eng verwandt.

pic/foreachgroup.png

Hinter jeder einzelnen Gruppierung (der current-group()) stehen aber noch gegebenenfalls mehrere Einzelwerte, die Sie 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>

Siehe auch und .

Zusammengesetzte Schlüssel

Daneben besteht die Möglichkeit, über die -Funktion zusammengesetzte Schlüssel zu erstellen. In der ausgewählten Nodeliste "//Ort/Mensch/Kauf" haben Sie 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 Weise kommt jede relevante Kombination aus Ortsname und Artikelname zustande, die Sie 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 Sie nun noch die 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 dient beispielsweise dazu, mäßig strukturierten XML-Dokumenten nachträglich eine übersichtlichere Struktur verleihen. Dabei wird jedes Mal 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 gearbeitet.

Nun startet innerhalb der aktuellen Gruppierung eine weitere, sie fragt, ob das schließende Element idOrt heisst. 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 Sie mit .


<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 / 13. Mai 2018



Fragen? Anmerkungen? Tips?

Bitte nehmen Sie Kontakt zu mir auf.






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