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 5-17, on top
of the str:dup
template you created in Recipe 1.5.
Example 5-17. Generic text-justification template—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' "/> <!-- 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 5-18 and Example 5-19 do this for
the person attributes in people.xml
. A similar
solution could be used for element encoding used in
people-elem.xml
.
Example 5-18. 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 5-19. Output
Name Age Sex Smoker --------------------|------|-----|--------- Al Zehtooney | 33| m | no Brad York | 38| m | yes Charles Xavier | 32| m | no David Willimas | 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 columns—in 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 5-20.
Example 5-20. 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( ) <= $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>
</xsl:text> </xsl:template> </xsl:stylesheet>
We can use these templates as shown in Example 5-21 to Example 5-23.
Example 5-21. 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 5-22. 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 5-23. 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 |
The problem of transforming element- or attribute-encoded data into columns is structurally similar to the delimited problem discussed in Recipe 5.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 5-24 to Example 5-26 show the attribute-based solution.
Example 5-24. 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>
</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>
</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>
</xsl:text> </xsl:template> <xsl:template match="@*" mode="text:map-col-value"> <xsl:value-of select="."/> </xsl:template>
Example 5-25. 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 5-26. Output (with gutter param = " | “)
Name | Age | Gender | Smoker | ------------------------------------------------- Al Zehtooney | 33 | male | no | Brad York | 38 | male | yes | Charles Xavier | 32 | male | no | David Willimas | 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 |