WhiteSpirit
WhiteSpirit

Reputation: 3

How to transform a sorted list into a table, with multiple values per cell

I have a list of items, sorted.
And I want to display them in a table with 4 columns, and multiple rows (depending on the size of the list, that may vary).

Each cell of the table is filled with exact 9 entries. The first cell should contain the 9 first entries, the second cell should contain entries starting form 10 to 18, the third cell from 19 tot 27, the fourth cell form 28 to 36 finishing the first row.

First cell of row 2 should start with entries 37 to 45, and so on....

To do so I created an XSLT file with the for-each instruction

<xsl:for-each select="catalog/cd[position() &lt; '10']"> 

and I do my check on the position() parameter.

In the second cell I use the statement :

<xsl:for-each select="catalog/cd[position() &gt; '9' and position() &lt; '19']">

But it doesn't work at all, although, this seems logical to me. I'm very new to XML and XSLT, so if anyone can help me please ?

I used the example from the W3schools ..

So this is my XML (standard example)

<?xml version="1.0" encoding="UTF-8"?>
<catalog>
    <cd>
        <title>Empire Burlesque</title>
        <artist>Bob Dylan</artist>
        <country>USA</country>
        <company>Columbia</company>
        <price>10.90</price>
        <year>1985</year>
    </cd>
    <cd>
        <title>Hide your heart</title>
        <artist>Bonnie Tyler</artist>
        <country>UK</country>
        <company>CBS Records</company>
        <price>9.90</price>
        <year>1988</year>
    </cd>
    <cd>
        <title>Greatest Hits</title>
        <artist>Dolly Parton</artist>
        <country>USA</country>
        <company>RCA</company>
        <price>9.90</price>
        <year>1982</year>
    </cd>
    <cd>
        <title>Still got the blues</title>
        <artist>Gary Moore</artist>
        <country>UK</country>
        <company>Virgin records</company>
        <price>10.20</price>
        <year>1990</year>
    </cd>
    <cd>
        <title>Eros</title>
        <artist>Eros Ramazzotti</artist>
        <country>EU</country>
        <company>BMG</company>
        <price>9.90</price>
        <year>1997</year>
    </cd>
    <cd>
        <title>One night only</title>
        <artist>Bee Gees</artist>
        <country>UK</country>
        <company>Polydor</company>
        <price>10.90</price>
        <year>1998</year>
    </cd>
    <cd>
        <title>Sylvias Mother</title>
        <artist>Dr.Hook</artist>
        <country>UK</country>
        <company>CBS</company>
        <price>8.10</price>
        <year>1973</year>
    </cd>
    <cd>
        <title>Maggie May</title>
        <artist>Rod Stewart</artist>
        <country>UK</country>
        <company>Pickwick</company>
        <price>8.50</price>
        <year>1990</year>
    </cd>
    <cd>
        <title>Romanza</title>
        <artist>Andrea Bocelli</artist>
        <country>EU</country>
        <company>Polydor</company>
        <price>10.80</price>
        <year>1996</year>
    </cd>
    <cd>
        <title>When a man loves a woman</title>
        <artist>Percy Sledge</artist>
        <country>USA</country>
        <company>Atlantic</company>
        <price>8.70</price>
        <year>1987</year>
    </cd>
    <cd>
        <title>Black angel</title>
        <artist>Savage Rose</artist>
        <country>EU</country>
        <company>Mega</company>
        <price>10.90</price>
        <year>1995</year>
    </cd>
    <cd>
        <title>1999 Grammy Nominees</title>
        <artist>Many</artist>
        <country>USA</country>
        <company>Grammy</company>
        <price>10.20</price>
        <year>1999</year>
    </cd>
    <cd>
        <title>For the good times</title>
        <artist>Kenny Rogers</artist>
        <country>UK</country>
        <company>Mucik Master</company>
        <price>8.70</price>
        <year>1995</year>
    </cd>
    <cd>
        <title>Big Willie style</title>
        <artist>Will Smith</artist>
        <country>USA</country>
        <company>Columbia</company>
        <price>9.90</price>
        <year>1997</year>
    </cd>
    <cd>
        <title>Tupelo Honey</title>
        <artist>Van Morrison</artist>
        <country>UK</country>
        <company>Polydor</company>
        <price>8.20</price>
        <year>1971</year>
    </cd>
    <cd>
        <title>Soulsville</title>
        <artist>Jorn Hoel</artist>
        <country>Norway</country>
        <company>WEA</company>
        <price>7.90</price>
        <year>1996</year>
    </cd>
    <cd>
        <title>The very best of</title>
        <artist>Cat Stevens</artist>
        <country>UK</country>
        <company>Island</company>
        <price>8.90</price>
        <year>1990</year>
    </cd>
    <cd>
        <title>Stop</title>
        <artist>Sam Brown</artist>
        <country>UK</country>
        <company>A and M</company>
        <price>8.90</price>
        <year>1988</year>
    </cd>
    <cd>
        <title>Bridge of Spies</title>
        <artist>T`Pau</artist>
        <country>UK</country>
        <company>Siren</company>
        <price>7.90</price>
        <year>1987</year>
    </cd>
</catalog>

I reduced my XSLT to only do the test for the first 2 columns :

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

    <xsl:template match="/">
        <html>
            <body>
                <table border="1">
                    <tr bgcolor="#D7BDE2">
                        <th>Blok1</th>
                        <th>Blok2</th>
                        <th>Blok3</th>
                        <th>Blok4</th>
                    </tr>
                    <td>
                        <table border="1">
                            <tr bgcolor="#9acd32">
                                <th>Title1</th>
                                <th>Artist</th>
                            </tr>
                            <xsl:for-each select="catalog/cd[position() &lt; '10']">
                                <tr>
                                    <td>
                                        <xsl:value-of select="title"/>
                                    </td>
                                    <td>
                                        <xsl:value-of select="artist"/>
                                    </td>
                                </tr>
                            </xsl:for-each>
                        </table>
                    </td>
                    <td>
                        <table border="1">
                            <tr bgcolor="#9acd32">
                                <th>Title2</th>
                                <th>Artist</th>
                            </tr>                            
                            <xsl:for-each select="catalog/cd[
   position() &gt; '9' AND position() &lt; '19' ]">
                                <tr>
                                    <td>
                                        <xsl:value-of select="title"/>
                                    </td>
                                    <td>
                                        <xsl:value-of select="artist"/>
                                    </td>
                                </tr>
                            </xsl:for-each>
                        </table>
                    </td>
                </table>
            </body>
        </html>
    </xsl:template>

</xsl:stylesheet>

Thank you very much to help me out .

PS Output should look something like this (see picture). First 9 CD's in first column, the following 9 in next column and so on.. enter image description here

Upvotes: 0

Views: 258

Answers (2)

zx485
zx485

Reputation: 29042

You can achieve a 9x4 solution with the following templates. There are two variables containing the dimensions at the root level. Its templates use a mode attribute for differentiating between the two dimensions, so if you want to add more dimensions (than 2) you'd have to introduce more modes.

The construction of the following templates is the following:

  1. Execute apply-templates with the first 36 ($vertical*$horizontal) cd elements with the attribute mode="hor"
  2. Add a <tr> element around the following elements and apply-templates with the first 36 ($vertical*$horizontal) cd elements again
  3. Add a table for the following 9 elements (position() mod $vertical = 1). This is applied to the current cd element itself and the following 8 elements with this expression: self::cd | following::cd[position() &lt; $vertical].

This is the XSLT:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:variable name="vertical" select="9" />    <!-- count per table -->
    <xsl:variable name="horizontal" select="4" />  <!-- horizontal count of tables -->

    <xsl:template match="/">
        <html>
            <body>
                <table border="1">
                    <tr bgcolor="#D7BDE2">
                        <th>Blok1</th>
                        <th>Blok2</th>
                        <th>Blok3</th>
                        <th>Blok4</th>
                    </tr>
                  <xsl:apply-templates select="catalog/cd[position() mod ($vertical*$horizontal) = 1]" mode="hor"/>
                </table>
            </body>
        </html>
    </xsl:template>

    <xsl:template match="cd" mode="hor">
        <tr>
            <xsl:apply-templates select="self::cd | following::cd[position() &lt; ($vertical*$horizontal)]" />
        </tr>    
    </xsl:template>

    <xsl:template match="cd[position() mod $vertical = 1]">
      <td>
        <table border="1">
            <tr bgcolor="#9acd32">
                <th>Title1</th>
                <th>Artist</th>
            </tr>
            <xsl:for-each select="self::cd | following::cd[position() &lt; $vertical]">
                <tr>
                    <td>
                        <xsl:value-of select="title"/>
                    </td>
                    <td>
                        <xsl:value-of select="artist"/>
                    </td>
                </tr>
            </xsl:for-each>
        </table>
        </td>
    </xsl:template>

    <xsl:template match="text()" />

</xsl:stylesheet>

The output should be a pleasant 4x9 matrix.

Upvotes: 0

John Bollinger
John Bollinger

Reputation: 180998

Use of xsl:for-each always has a bit of code smell to me. It has its uses, I guess, but I'm generally inclined to prefer xsl:apply-templates and a separate top-level template . The fact that you have highly duplicative code in several places has worse code smell: that's just begging to be merged into a common template. I observe also that the approach you are attempting is predicated on being able to write a separate, for-each for each group of nine CDs, but that just doesn't scale.

There are multiple ways you could approach this, but I'm going to suggest one based on aligning templates with your various groupings of elements, and using modes to distinguish. First the top level:

<xsl:template match="/catalog">
  <html>
    <body>
      <table>
        <tr><th>Block1</th><th>Block2</th><th>Block3</th><th>Block4</th></tr>
        <xsl:apply-templates
            select="cd[floor((position() - 1) div 36) = ((position() - 1) div 36)]"
            mode="row"/>
      </table>
    </body>
  </html>
</xsl:template>

That sets up the outer framework, and then, within the outer <table>, it selects the first of each group of 36 cds for transformation in mode "row". These, the first element of the first block of each row, serve as reference points. The row transformation looks like this:

<xsl:template match="cd" mode="row">
  <tr><xsl:apply-templates
      select="self::node()|following-sibling::cd[position() = 9 or position() = 18 or position = 27]"
      mode="block"/></tr>
</xsl:template>

That's pretty easy. It just creates the needed <tr> for each top-level row, and then transforms the context node, and its siblings 9, 18, and 27 elements following, according to mode "block". This is a similar deal as the last one: the elements selected for transformation are the first of each block in the same row, and they will serve as reference points for their blocks. The block transformation is along similar lines:

<xsl:template match="cd" mode="block">
  <td><table>
    <tr><th>Title</th><th>Artist</th></tr>
    <xsl:apply-templates
        select="self::node()|following-sibling::cd[position() &lt; 9]"
        mode="album"/>
  </table></td>
</xsl:template>

That has a little more content, because we need to set up the inner per-block table, but we again use the following-sibling axis to select the context node's peers for transformation, this time the others in the same block. The final template presents individual CD data:

<xsl:template match="cd" mode="album">
  <tr><td><xsl:value-of select="title"/></td><td><xsl:value-of select="artist"/></td></tr>
</xsl:template>

That last is pretty much along the same lines you used, just packaged differently.

Note that this emits well-formed HTML (yours is missing some <tr>s, though it's so complex that that's easy to miss), and it's nice and modular. It does not including any styling, and as a result, my browser's rendition of it is as ugly as sin, but I'd advise correcting that by adding appropriate CSS, or even an external CSS stylesheet.

Upvotes: 1

Related Questions