Einen in Spalten gesetzten Bericht erzeugen

(Auszug aus "XSLT Kochbuch" von Sal Mangano)

Problem

Sie wollen Daten zur Präsentation in Spalten formatieren.

Lösung

Es gibt zwei allgemeine Arten, wie man XML auf Spalten abbilden kann. Die erste ordnet unterschiedliche Elemente oder Attribute getrennten Spalten zu. Die zweite bildet die Elemente auf der Grundlage ihrer relativen Position ab.

Bevor Sie diese Variationen in Angriff nehmen, brauchen Sie ein generisches Template, das Ihnen hilft, Ausgabetext in einer Spalte fester Breite auszurichten. Sie können eine solche Routine, die im folgenden Beispiel zu sehen ist, auf der Grundlage des str:dup-Templates aufbauen, das Sie im Rezept Einen String n-mal duplizieren erzeugt haben.

Beispiel: Generisches Template zum Ausrichten von Text – text.justify.xslt.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:str="http://www.ora.com/XSLTCookbook/namespaces/strings" xmlns:text="http://www.ora.com/XSLTCookbook/namespaces/text" extension-element-prefixes="text">
  <xsl:include href="../strings/str.dup.xslt"/>
  <xsl:template name="text:justify">
    <xsl:param name="value" />
    <xsl:param name="width" select="10"/>
    <xsl:param name="align" select=" 'left' "/>
    <!-- Abschneiden, falls zu lang -->
    <xsl:variable name="output" select="substring($value,1,$width)"/>
    <xsl:choose>
      <xsl:when test="$align = 'left'">
        <xsl:value-of select="$output"/>
        <xsl:call-template name="str:dup">
          <xsl:with-param name="input" select=" ' ' "/>
          <xsl:with-param name="count" select="$width - string-length($output)"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:when test="$align = 'right'">
        <xsl:call-template name="str:dup">
          <xsl:with-param name="input" select=" ' ' "/>
          <xsl:with-param name="count" select="$width - string-length($output)"/>
        </xsl:call-template>
        <xsl:value-of select="$output"/>
      </xsl:when>
      <xsl:when test="$align = 'center'">
        <xsl:call-template name="str:dup">
          <xsl:with-param name="input" select=" ' ' "/>
          <xsl:with-param name="count" select="floor(($width - string-length($output)) div 2)"/>
        </xsl:call-template>
        <xsl:value-of select="$output"/>
        <xsl:call-template name="str:dup">
          <xsl:with-param name="input" select=" ' ' "/>
          <xsl:with-param name="count" select="ceiling(($width - string-length($output)) div 2)"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>INVALID ALIGN</xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

Mit diesem Template ist das Erzeugen einer Spaltendarstellung nur noch eine Angelegenheit des Festlegens der Reihenfolge und der Spaltenlayouts für die Daten. Die beiden folgenden Beispiele erledigen dies für die person-Attribute in people.xml. Eine ähnliche Lösung könnte für die Elementkodierung verwendet werden, die in people-elem.xml zum Einsatz kommt.

Beispiel: people-to-columns.xslt.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:str="http://www.ora.com/XSLTCookbook/namespaces/strings" xmlns:text="http://www.ora.com/XSLTCookbook/namespaces/text">
  <xsl:include href="text.justify.xslt"/>
  <xsl:output method="text" />
  <xsl:strip-space elements="*"/>
  <xsl:template match="people">
    Name                  |    Alter  | Geschlecht | Raucher |
    ----------------------------------------------------------
    <xsl:apply-templates/>
  </xsl:template>
  <xsl:template match="person">
    <xsl:call-template name="text:justify">
      <xsl:with-param name="value" select="@name"/>
      <xsl:with-param name="width" select="20"/>
    </xsl:call-template>
    <xsl:text>|</xsl:text>
    <xsl:call-template name="text:justify">
      <xsl:with-param name="value" select="@age"/>
      <xsl:with-param name="width" select="8"/>
      <xsl:with-param name="align" select=" 'right' "/>
    </xsl:call-template>
    <xsl:text>|</xsl:text>
    <xsl:call-template name="text:justify">
      <xsl:with-param name="value" select="@sex"/>
      <xsl:with-param name="width" select="11"/>
      <xsl:with-param name="align" select=" 'center' "/>
    </xsl:call-template>
    <xsl:text>|</xsl:text>
    <xsl:call-template name="text:justify">
      <xsl:with-param name="value" select="@smoker"/>
      <xsl:with-param name="width" select="10"/>
      <xsl:with-param name="align" select=" 'center' "/>
    </xsl:call-template>
    <xsl:text> </xsl:text>
  </xsl:template>
</xsl:stylesheet>

Beispiel: Ausgabe.

Name | Alter | Geschlecht | Raucher |
-----------------------------------------------------------------------
Al Zehtooney | 33 | m | nein |
Brad York | 38 | m | ja |
Charles Xavier | 32 | m | nein |
David Williams | 33 | m | nein |
Edward Ulster | 33 | m | ja |
Frank Townsend | 35 | m | nein |
Greg Sutter | 40 | m | nein |
Harry Rogers | 37 | m | nein |
John Quincy | 43 | m | ja |
Kent Peterson | 31 | m | nein |
Larry Newell | 23 | m | nein |
Max Milton | 22 | m | nein |
Norman Lamagna | 30 | m | nein |
Ollie Kensinton | 44 | m | nein |
John Frank | 24 | m | nein |
Mary Williams | 33 | w | nein |
Jane Frank | 38 | w | ja |
Jo Peterson | 32 | w | nein |
Angie Frost | 33 | w | nein |
Betty Bates | 33 | w | nein |
Connie Date | 35 | w | nein |
Donna Finster | 20 | w | nein |
Esther Gates | 37 | w | nein |
Fanny Hill | 33 | w | ja |
Geta Iota | 27 | w | nein |
Hillary Johnson | 22 | w | nein |
Ingrid Kent | 21 | w | nein |
Jill Larson | 20 | w | nein |
Kim Mulrooney | 41 | w | nein |
Lisa Nevins | 21 | w | nein |

Um Daten basierend auf ihrer Position im Dokument zu transformieren, müssen Sie einen etwas anderen Ansatz verfolgen. Entscheiden Sie als Erstes, wie viele Spalten Sie haben werden. Sie können einen Parameter benutzen, der die Anzahl der Spalten bestimmt, und es erlauben, die Anzahl der Zeilen entsprechend der Anzahl der Elemente festzulegen. Oder Sie geben die Anzahl der Zeilen an und lassen die Spalten variieren. Als Zweites entscheiden Sie, wie die Position des Elements auf die Spalten abgebildet wird. Die beiden gebräuchlichsten Zuordnungen sind zeilenweise (row-major) und spaltenweise (column-major). Bei der zeilenweisen Zuordnung wird das erste Element auf die erste Spalte abgebildet, das zweite Element auf die zweite Spalte usw., bis die Spalten zu Ende sind – in diesem Fall beginnen Sie eine neue Zeile. Bei der spaltenweisen Zuordnung kommen die ersten (N div Anzahl-der-Spalten) Elemente in die erste Spalte, die nächsten (N div Anzahl-der-Spalten) Elemente kommen in die zweite Spalte usw. Sie können sich dieses Konzept einfacher als eine Art Transposition von Zeilen in Spalten vorstellen.

Sie können zwei Templates erzeugen, die Spalten in jeder Anordnung ausgeben, wie im folgenden Beispiel gezeigt wird.

Beispiel: text.matrix.xslt

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:text="http://www.ora.com/XSLTCookbook/namespaces/text" extension-element-prefixes="text">
  <xsl:output method="text"/>
  <xsl:include href="text.justify.xslt"/>
  <xsl:template name="text:row-major">
    <xsl:param name="nodes" select="/.."/>
    <xsl:param name="num-cols" select="2"/>
    <xsl:param name="width" select="10"/>
    <xsl:param name="align" select=" 'left' "/>
    <xsl:param name="gutter" select=" ' ' "/>
    <xsl:if test="$nodes">
      <xsl:call-template name="text:row">
        <xsl:with-param name="nodes" select="$nodes[position() &lt;= $num-cols]"/>
        <xsl:with-param name="width" select="$width"/>
        <xsl:with-param name="align" select="$align"/>
        <xsl:with-param name="gutter" select="$gutter"/>
      </xsl:call-template>
      <!-- Verarbeiten der verbleibenden Zeilen -->
      <xsl:call-template name="text:row-major">
        <xsl:with-param name="nodes" select="$nodes[position() &gt; $num-cols]"/>
        <xsl:with-param name="num-cols" select="$num-cols"/>
        <xsl:with-param name="width" select="$width"/>
        <xsl:with-param name="align" select="$align"/>
        <xsl:with-param name="gutter" select="$gutter"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>
  <xsl:template name="text:col-major">
    <xsl:param name="nodes" select="/.."/>
    <xsl:param name="num-cols" select="2"/>
    <xsl:param name="width" select="10"/>
    <xsl:param name="align" select=" 'left' "/>
    <xsl:param name="gutter" select=" ' ' "/>
    <xsl:if test="$nodes">
      <xsl:call-template name="text:row">
        <xsl:with-param name="nodes" select="$nodes[(position() - 1) mod ceiling(last() div $num-cols) = 0]"/>
        <xsl:with-param name="width" select="$width"/>
        <xsl:with-param name="align" select="$align"/>
        <xsl:with-param name="gutter" select="$gutter"/>
      </xsl:call-template>
      <!-- Verarbeiten der verbleibenden Zeilen -->
      <xsl:call-template name="text:col-major">
        <xsl:with-param name="nodes" select="$nodes[(position() - 1) mod ceiling(last() div $num-cols) != 0]"/>
        <xsl:with-param name="num-cols" select="$num-cols"/>
        <xsl:with-param name="width" select="$width"/>
        <xsl:with-param name="align" select="$align"/>
        <xsl:with-param name="gutter" select="$gutter"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>
  <xsl:template name="text:row">
    <xsl:param name="nodes" select="/.."/>
    <xsl:param name="width" select="10"/>
    <xsl:param name="align" select=" 'left' "/>
    <xsl:param name="gutter" select=" ' ' "/>
    <xsl:for-each select="$nodes">
      <xsl:call-template name="text:justify">
        <xsl:with-param name="value" select="."/>
        <xsl:with-param name="width" select="$width"/>
        <xsl:with-param name="align" select="$align"/>
      </xsl:call-template>
      <xsl:value-of select="$gutter"/>
    </xsl:for-each>
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>
 </xsl:stylesheet>

Wir können diese Templates einsetzen, wie in den drei folgenden Beispielen gezeigt wird.

Beispiel: Eingabe.

<numbers>
  <number>10</number>
  <number>3.5</number>
  <number>4.44</number>
  <number>77.7777</number>
  <number>-8</number>
  <number>1</number>
  <number>444</number>
  <number>1.1234</number>
  <number>7.77</number>
  <number>3.1415927</number>
  <number>10</number>
  <number>9</number>
  <number>8</number>
  <number>7</number>
  <number>666</number>
  <number>5555</number>
  <number>-4444444</number>
  <number>22.33</number>
  <number>18</number>
  <number>36.54</number>
  <number>43</number>
  <number>99999</number>
  <number>999999</number>
  <number>9999999</number>
  <number>32</number>
  <number>64</number>
  <number>-64.0001</number>
</numbers>

Beispiel: Stylesheet.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:text="http://www.ora.com/XSLTCookbook/namespaces/text">
  <xsl:output method="text" />
  <xsl:include href="text.matrix.xslt"/>
  <xsl:template match="numbers">
    Fünf Spalten mit Zahlen in zeilenweiser Anordnung:
    <xsl:text/>
    <xsl:call-template name="text:row-major">
      <xsl:with-param name="nodes" select="number"/>
      <xsl:with-param name="align" select=" 'right' "/>
      <xsl:with-param name="num-cols" select="5"/>
      <xsl:with-param name="gutter" select=" ' | ' "/>
    </xsl:call-template>
    Fünf Spalten mit Zahlen in spaltenweiser Anordnung:
    <xsl:text/>
    <xsl:call-template name="text:col-major">
      <xsl:with-param name="nodes" select="number"/>
      <xsl:with-param name="align" select=" 'right' "/>
      <xsl:with-param name="num-cols" select="5"/>
      <xsl:with-param name="gutter" select=" ' | ' "/>
    </xsl:call-template>
  </xsl:template>
</xsl:stylesheet>

Beispiel: Ausgabe.

Fünf Spalten mit Zahlen in zeilenweiser Anordnung:

10 | 3.5 | 4.44 | 77.7777 | -8 |
1 | 444 | 1.1234 | 7.77 | 3.1415927 |
10 | 9 | 8 | 7 | 666 |
5555| -4444444 | 22.33| 18 | 36.54 |
43 | 99999 | 999999 | 999999 | 32 |
64 | -64.0001 |

Fünf Spalten mit Zahlen in spaltenweiser Anordnung:

10 | 444 | 8 | 18 | 32 |
3.5 | 1.1234 | 7 | 36.54 | 64 |
4.44 | 7.77 | 666 | 43 | −64.0001 |
777.777 | 3.1415927 | 5555 | 99999 |
−8 | 10 | −4444444 | 999999 |
1 | 9 | 22.33 | 9999999 |

XSLT 2.0

Die wichtigste Verbesserung, die Sie in XSLT 2.0 machen können, besteht darin, das Template text:justify in eine Funktion umzuwandeln und die Eigenschaften von XPath 2.0 zu verwenden, um es knapper zu gestalten. Sie können die Funktion string-join zusammen mit einem for-Ausdruck einsetzen, um eine Füllfunktion zu erzeugen, mit der Sie die richtige Menge an Whitespace einfügen. Außerdem können Sie text:justify überladen, um die Wirkung eines vorgegebenen Parameters für die Ausrichtung zu erzielen:

<xsl:function name="text:dup" as="xs:string">
  <xsl:param name="input" as="xs:string"/>
  <xsl:param name="count" as="xs:integer"/>
  <xsl:sequence select="string-join(for $i in 1 to $count return $input, '')"/>
</xsl:function>
<xsl:function name="text:justify" as="xs:string">
  <xsl:param name="value" as="xs:string"/>
  <xsl:param name="width" as="xs:integer" />
  <xsl:sequence select="text:justify($value, $width, 'left')"/>
</xsl:function>
<xsl:function name="text:justify" as="xs:string">
  <xsl:param name="value" as="xs:string"/>
  <xsl:param name="width" as="xs:integer" />
  <xsl:param name="align" as="xs:string" />
  <!-- Abschneiden, falls zu lang -->
  <xsl:variable name="output" select="substring($value,1,$width)" as="xs:string"/>
  <xsl:variable name="offset" select="$width - string-length($output)" as="xs:integer"/>
  <xsl:choose>
    <xsl:when test="$align = 'left'">
      <xsl:value-of select="concat($output, text:dup(' ', $offset))"/>
    </xsl:when>
    <xsl:when test="$align = 'right'">
      <xsl:value-of select="concat(text:dup(' ', $offset), $output)"/>
    </xsl:when>
    <xsl:when test="$align = 'center'">
      <xsl:variable name="before" select="$offset idiv 2"/>
      <xsl:variable name="after" select="$before + $offset mod 2"/>
      <xsl:value-of select="concat(text:dup(' ', $before), $output,text:dup(' ', $after))"/>
    </xsl:when>
    <xsl:otherwise>INVALID ALIGN</xsl:otherwise>
  </xsl:choose>
</xsl:function>

Diskussion

Das Problem der Transformation element- oder attributkodierter Daten in Spalten ähnelt von der Struktur her dem Problem der abgetrennten Daten, das im Rezept XML in Daten mit Trennzeichen exportieren besprochen wurde. Der größte Unterschied besteht darin, dass Sie im Fall der abgetrennten Daten die Daten für die Maschinenverarbeitung vorbereiten. Im aktuellen Fall bereiten Sie sie für die menschliche Verarbeitung vor. In mancher Hinsicht sind Menschen penibler als Maschinen, vor allem, wenn es um Ausrichtung und andere visuelle Hilfen geht, die ein leichtes Verstehen ermöglichen. Sie könnten den gleichen datengetriebenen generischen Ansatz verwenden wie in dem Beispiel mit den abgetrennten Daten, müssten aber mehr Informationen über die einzelnen Spalten liefern, um die richtige Formatierung zu gewährleisten. Die drei folgenden Beispiele zeigen die attributbasierte Lösung.

Beispiel: generic-attr-to-columns.xslt

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:str="http://www.ora.com/XSLTCookbook/namespaces/strings" xmlns:text="http://www.ora.com/XSLTCookbook/namespaces/text">
  <xsl:include href="text.justify.xslt"/>
  <xsl:param name="gutter" select=" ' ' "/>
  <xsl:output method="text"/>
  <xsl:strip-space elements="*"/>
  <xsl:variable name="columns" select="/.."/>
  <xsl:template match="/">
    <xsl:for-each select="$columns">
      <xsl:call-template name="text:justify" >
        <xsl:with-param name="value" select="@name"/>
        <xsl:with-param name="width" select="@width"/>
        <xsl:with-param name="align" select=" 'left' "/>
      </xsl:call-template>
      <xsl:value-of select="$gutter"/>
    </xsl:for-each>
    <xsl:text>&#xa;</xsl:text>
    <xsl:for-each select="$columns">
      <xsl:call-template name="str:dup">
        <xsl:with-param name="input" select=" '-' "/>
        <xsl:with-param name="count" select="@width"/>
      </xsl:call-template>
      <xsl:call-template name="str:dup">
        <xsl:with-param name="input" select=" '-' "/>
        <xsl:with-param name="count" select="string-length($gutter)"/>
      </xsl:call-template>
    </xsl:for-each>
    <xsl:text>&#xa;</xsl:text>
    <xsl:apply-templates/>
  </xsl:template>
  <xsl:template match="/*/*">
    <xsl:variable name="row" select="."/>
    <xsl:for-each select="$columns">
      <xsl:variable name="value">
        <xsl:apply-templates select="$row/@*[local-name(.)=current()/@attr]" mode="text:map-col-value"/>
      </xsl:variable>
      <xsl:call-template name="text:justify" >
        <xsl:with-param name="width" select="@width"/>
        <xsl:with-param name="align" select="@align"/>
      </xsl:call-template>
      <xsl:value-of select="$gutter"/>
    </xsl:for-each>
    <xsl:text>&#xa;</xsl:text>
  </xsl:template>
  <xsl:template match="@*" mode="text:map-col-value">
    <xsl:value-of select="."/>
  </xsl:template>    
</xsl:stylesheet>

Beispiel: people-to-cols-using-generic.xslt

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:str="http://www.ora.com/XSLTCookbook/namespaces/strings" xmlns:text="http://www.ora.com/XSLTCookbook/namespaces/text">
  <xsl:import href="generic-attr-to-columns.xslt"/>
  <!--Definiert die Zuordnung von Attributen zu Spalten -->
  <xsl:variable name="columns" select="document('')/*/text:column"/>
  <text:column name="Name" width="20" align="left" attr="name"/>
  <text:column name="Alter" width="8" align="right" attr="age"/>
  <text:column name="Geschlecht" width="12" align="left" attr="sex"/>
  <text:column name="Raucher" width="9" align="left" attr="smoker"/>
  <!-- Verarbeitet eigene Attributzuordnungen -->
  <xsl:template match="@sex" mode="text:map-col-value">
    <xsl:choose>
      <xsl:when test=".='m'">männlich</xsl:when>
      <xsl:when test=".='w'">weiblich</xsl:when>
      <xsl:otherwise>error</xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

Beispiel: Ausgabe (mit dem Trennparameter » | «).

Name | Alter | Geschlecht | Raucher |
-----------------------------------------------------------------------
Al Zehtooney | 33 | männlich | nein |
Brad York | 38 | männlich | ja |
Charles Xavier | 32 | männlich | nein |
David Williams | 33 | männlich | nein |
Edward Ulster | 33 | männlich | ja |
Frank Townsend | 35 | männlich | nein |
Greg Sutter | 40 | männlich | nein |
Harry Rogers | 37 | männlich | nein |
John Quincy | 43 | männlich | ja |
Kent Peterson | 31 | männlich | nein |
Larry Newell | 23 | männlich | nein |
Max Milton | 22 | männlich | nein |
Norman Lamagna | 30 | männlich | nein |
Ollie Kensinton | 44 | männlich | nein |
John Frank | 24 | männlich | nein |
Mary Williams | 33 | weiblich | nein |
Jane Frank | 38 | weiblich | ja |
Jo Peterson | 32 | weiblich | nein |
Angie Frost | 33 | weiblich | nein |
Betty Bates | 33 | weiblich | nein |
Connie Date | 35 | weiblich | nein |
Donna Finster | 20 | weiblich | nein |
Esther Gates | 37 | weiblich | nein |
Fanny Hill | 33 | weiblich | ja |
Geta Iota | 27 | weiblich | nein |
Hillary Johnson | 22 | weiblich | nein |
Ingrid Kent | 21 | weiblich | nein |
Jill Larson | 20 | weiblich | nein |
Kim Mulrooney | 41 | weiblich | nein |
Lisa Nevins | 21 | weiblich | nein |
Tipp der data2type-Redaktion:
Zum Thema XSLT bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an:

Copyright © 2006 O'Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "XSLT Kochbuch" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

O'Reilly Verlag GmbH & Co. KG, Balthasarstraße 81, 50670 Köln, kommentar(at)oreilly.de