Recipe7.3.Creating a Columnar Report


Recipe 7.3. Creating a Columnar Report

Problem

You want to format data into columns for presentation.

Solution

There are two general kinds of XML-to-columnar mappings. The first maps different elements or attributes into separate columns. The second maps elements based on their relative position.

Before tackling these variations, you need a generic template that will help justify output text into a fixed-width column. You can build such a routine, shown in Example 7-19, on top of the str:dup template you created in Recipe 2.5.

Example 7-19. Generic text-justification templatetext.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' "/>       <!-- Truncate if too long -->     <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>

Given this template, producing a columnar report is simply a matter of deciding the order and column layouts for the data. Example 7-20 and Example 7-21 do this for the person attributes in people.xml. A similar solution could be used for element encoding used in people-elem.xml.

Example 7-20. 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                 Age    Sex   Smoker --------------------|------|-----|--------- <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="6"/>     <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="6"/>     <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="9"/>     <xsl:with-param name="align" select=" 'center' "/>   </xsl:call-template>   <xsl:text> </xsl:text>   </xsl:template>     </xsl:stylesheet>

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

To transform data based on its position in the document, you must take a slightly different approach. First, decide how many columns you will have. You can use a parameter that specifies the number of columns and allow the number of rows to follow based on the number of elements, or you can specify the number of rows and let the columns vary. Second, decide how the position of the element will map onto the columns. The two most common mappings are row major and column major. In row major, the first element maps to the first column, the second element maps to the second column, and so on until you run out of columnsin which case, you begin a new row. In column major, the first (N div num-columns) elements go into the first column, then the next (N div num-columns) elements go into the second column, and so on. You can think of this concept more simply in terms of a transposition of rows to columns.

You can create two templates that output columns in each order, as shown in Example 7-22.

Example 7-22. 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>         <!-- process remaining rows -->         <xsl:call-template name="text:row-major">           <xsl:with-param name="nodes"                 select="$nodes[position( ) > $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>                  <!-- process remaining rows -->         <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>

We can use these templates as shown in Example 7-23 to Example 7-25.

Example 7-23. Input
<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>

Example 7-24. 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"> Five columns of numbers in row major order: <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>     Five columns of numbers in column major order: <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>

Example 7-25. Output
Five columns of numbers in row major order:         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 |    9999999 |         32 |         64 |   -64.0001 |     Five columns of numbers in column major order:         10 |        444 |          8 |         18 |         32 |        3.5 |     1.1234 |          7 |      36.54 |         64 |       4.44 |       7.77 |        666 |         43 |   -64.0001 |    77.7777 |  3.1415927 |       5555 |      99999 |         -8 |         10 |   -4444444 |     999999 |          1 |          9 |      22.33 |    9999999 |

XSLT 2.0

The main improvement you can make in XSLT 2.0 is to convert the text:justify template to a function and use the features of XPath 2.0 to make it more concise. You can use the string-join function in conjunction with a for expression to create a dup function to insert the correct amount of white space padding. You can also overload text:justify to achieve the effect of a default parameter for the alignment:

<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" />       <!-- Truncate if too long -->     <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>

Discussion

The problem of transforming element- or attribute-encoded data into columns is structurally similar to the delimited problem discussed in Recipe 7.2. The main difference is that in the delimited case, you prepare data for machine processing and in the present case, you prepare the data for human processing. In some ways, humans are more finicky then machines, especially when it comes to alignment and other visual aids that facilitate easy comprehension. You could apply the same data-driven generic approach used in the delimited example, but you would have to provide more information about each column to ensure proper formatting. Example 7-26 to Example 7-28 show the attribute-based solution.

Example 7-26. 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="value" select="$value"/>       <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>

Example 7-27. 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"/>     <!--Defines the mapping from attributes to columns --> <xsl:variable name="columns" select="document('')/*/text:column"/>     <text:column name="Name" width="20" align="left" attr="name"/> <text:column name="Age" width="6" align="right" attr="age"/> <text:column name="Gender" width="6" align="left" attr="sex"/> <text:column name="Smoker" width="6" align="left" attr="smoker"/>     <!-- Handle custom attribute mappings -->     <xsl:template match="@sex" mode="text:map-col-value">   <xsl:choose>     <xsl:when test=".='m'">male</xsl:when>     <xsl:when test=".='f'">female</xsl:when>     <xsl:otherwise>error</xsl:otherwise>   </xsl:choose> </xsl:template>     </xsl:stylesheet>

Example 7-28. Output (with gutter param = " | ")
Name                 | Age    | Gender | Smoker | ------------------------------------------------- Al Zehtooney         |     33 | male   | no     | Brad York            |     38 | male   | yes    | Charles Xavier       |     32 | male   | no     | David Williams       |     33 | male   | no     | Edward Ulster        |     33 | male   | yes    | Frank Townsend       |     35 | male   | no     | Greg Sutter          |     40 | male   | no     | Harry Rogers         |     37 | male   | no     | John Quincy          |     43 | male   | yes    | Kent Peterson        |     31 | male   | no     | Larry Newell         |     23 | male   | no     | Max Milton           |     22 | male   | no     | Norman Lamagna       |     30 | male   | no     | Ollie Kensinton      |     44 | male   | no     | John Frank           |     24 | male   | no     | Mary Williams        |     33 | female | no     | Jane Frank           |     38 | female | yes    | Jo Peterson          |     32 | female | no     | Angie Frost          |     33 | female | no     | Betty Bates          |     33 | female | no     | Connie Date          |     35 | female | no     | Donna Finster        |     20 | female | no     | Esther Gates         |     37 | female | no     | Fanny Hill           |     33 | female | yes    | Geta Iota            |     27 | female | no     | Hillary Johnson      |     22 | female | no     | Ingrid Kent          |     21 | female | no     | Jill Larson          |     20 | female | no     | Kim Mulrooney        |     41 | female | no     | Lisa Nevins          |     21 | female | no     |




XSLT Cookbook
XSLT Cookbook: Solutions and Examples for XML and XSLT Developers, 2nd Edition
ISBN: 0596009747
EAN: 2147483647
Year: 2003
Pages: 208
Authors: Sal Mangano

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net