tags:

views:

2398

answers:

4

Hi

I have the following xsl that sorts my xml alphabetically:

<xsl:template match="/">
  <xsl:apply-templates />
</xsl:template> 

<xsl:key name="rows-by-title" match="Row" use="translate(substring(@Title,1,1),'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
<xsl:variable name="StartRow" select="string('&lt;tr &gt;')" />

<xsl:template name="Meunchian" match="/dsQueryResponse/Rows">
  <table>
    <tr>
      <xsl:for-each select="Row[count(. | key('rows-by-title', translate(substring(@Title,1,1),'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLMNOPQRSTUVWXYZ'))[1]) = 1]">
        <xsl:sort select="translate(substring(@Title,1,1),'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
        <!-- Puts out the title -->
        <td>
          <xsl:value-of select="translate(substring(@Title,1,1),'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
        </td>
        <!-- Now all it's children -->
        <xsl:for-each select="key('rows-by-title', translate(substring(@Title,1,1),'abcdefghijklmnopqrstuvwxyz','ABCDEFGHIJKLMNOPQRSTUVWXYZ'))">
          <xsl:value-of select="@Title" /><br/>
        </xsl:for-each>
      </xsl:for-each>
   </tr>
  </table>
</xsl:template>

XML:

<dsQueryResponse>
  <Rows>
    <Row Title="Agenda" />
    <Row Title="Policy" />
    <Row Title="Policy" />
    <Row Title="Report" />
    <Row Title="Report" />
  </Rows>
</dsQueryResponse>

I now want to break the table row every 4 columns that are output so that the output looks something like:

ABCD
EFGH
IJKL
MNOP
QRST
UVWX
YZ

Can anyone suggest the best way to achieve this?

Many Thanks

A: 

I'm sort of confused by the question but I think what you're looking for is an xsl:if test with a combination of position() and mod

annakata
Care to explain this one Dimitre or did you just find an answer you hadn't already downvoted?
annakata
+3  A: 

Here is my solution.

You can decide via parameters "per-row" and "show-empty" if you want empty cells to show up or if you want to hide them. I'm sure a much more elegant version exists, but I could not come up with one. ;-) Comments welcome.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
  <xsl:output method="html" version="4.0" encoding="iso-8859-1" indent="yes"/>

  <xsl:key name="rows-by-title" match="Row" use="translate(substring(@Title, 1, 1), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
  <xsl:variable name="alphabet" select="string('ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
  <xsl:variable name="per-row" select="number(4)" /> 
  <xsl:variable name="show-empty" select="false()" />

  <xsl:template match="/">
    <xsl:apply-templates select="dsQueryResponse/Rows" />
  </xsl:template> 

  <xsl:template match="Rows">
    <table>
      <xsl:call-template name="create-rows" />
    </table>
  </xsl:template>

  <xsl:template name="create-rows">
    <xsl:param name="index" select="1" />

    <xsl:variable name="letters">
      <xsl:call-template name="next-letters">
        <xsl:with-param name="index" select="$index" />
      </xsl:call-template>
    </xsl:variable>

    <xsl:if test="$letters != ''">
      <tr title="{$letters}">
        <xsl:call-template name="create-cells">
          <xsl:with-param name="letters" select="$letters" />
        </xsl:call-template>
      </tr>
    </xsl:if>

    <xsl:if test="string-length($letters) = $per-row">
      <xsl:call-template name="create-rows">
        <xsl:with-param name="index" select="string-length(substring-before($alphabet, substring($letters, string-length($letters), 1))) + 2" />
      </xsl:call-template>
    </xsl:if>
  </xsl:template>

  <xsl:template name="next-letters">
    <xsl:param name="index" />

    <xsl:variable name="letter" select="substring($alphabet, $index, 1)" />

    <xsl:variable name="letters">
      <xsl:if test="$index &lt;= string-length($alphabet)">
        <xsl:if test="$show-empty or key('rows-by-title', $letter)">
          <xsl:value-of select="$letter" />
        </xsl:if>

        <xsl:call-template name="next-letters">
          <xsl:with-param name="index" select="$index + 1" />
        </xsl:call-template>
      </xsl:if>
    </xsl:variable>

    <xsl:value-of select="substring($letters, 1, $per-row)" />
  </xsl:template>

  <xsl:template name="create-cells">
    <xsl:param name="letters" />

    <xsl:variable name="letter" select="substring($letters, 1, 1)" />

    <xsl:if test="$letter != ''">
      <td title="{$letter}">
        <strong>
          <xsl:value-of select="$letter" />
        </strong>
        <xsl:apply-templates select="key('rows-by-title', $letter)">
          <xsl:sort select="@Title" />
        </xsl:apply-templates>
      </td>
      <xsl:call-template name="create-cells">
        <xsl:with-param name="letters" select="substring($letters, 2, string-length($letters) - 1)" />
      </xsl:call-template>
    </xsl:if>
  </xsl:template>

  <xsl:template match="Row">
    <br />
    <xsl:value-of select="@Title" />
  </xsl:template>

</xsl:stylesheet>

With this input:

<dsQueryResponse>
  <Rows>
    <Row Title="Agenda" />
    <Row Title="Policy" />
    <Row Title="Policy" />
    <Row Title="Report" />
    <Row Title="Report" />
    <Row Title="Test2" />
    <Row Title="Test1" />
    <Row Title="Boo" />
    <Row Title="Foo" />
  </Rows>
</dsQueryResponse>

This output is produced (the title attributes were just for debugging. I left them in, remove them anytime):

<table>
  <tr title="ABFP">
    <td title="A">
      <strong>A</strong>
      <br>Agenda
    </td>
    <td title="B">
      <strong>B</strong>
      <br>Boo
    </td>
    <td title="F">
      <strong>F</strong>
      <br>Foo
    </td>
    <td title="P">
      <strong>P</strong>
      <br>Policy
      <br>Policy
    </td>
  </tr>
  <tr title="RT">
    <td title="R">
      <strong>R</strong>
      <br>Report
      <br>Report
    </td>
    <td title="T">
      <strong>T</strong>
      <br>Test1
      <br>Test2
    </td>
  </tr>
</table>
Tomalak
@Tomalak: Don't worry, I deleted and re-submitted my answer.
Dimitre Novatchev
@Tomalak: +1. Good solution! It avoids the need for the xx:node-set() extension, but may be a little bit not so efficient. Once I also had fun with creating such a solution without extensions -- must search the newsgroups. Seems around twice longer than my solution
Dimitre Novatchev
I'm sure there is room for improvement. I don't like the sequential "letters for each row" approach, but it's the best I could think of, given the fact that I wanted empty cells "displayable". Can this be done through pure contextual recursion, but still without extension functions?
Tomalak
Dimitre Novatchev
Thanks, bookmarked. Can't read it right now, unfortunately.
Tomalak
@Tomalak: See my second answer for a solution, which doesn't use extensions and is not recursive. BTW, I couldn't format the text as code: pressing ~ just replaced the selection. Did I understand you correctly?
Dimitre Novatchev
+1  A: 

This question must be edited in order for anyone to understand what the problem really is. The comment by Tomalak reveals that the OP "wants lists of items in an alphabetically ordered grid. One list for each letter. Four letters horizontally, as much as it takes vertically"

The following transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:ext="http://exslt.org/common"
 extension-element-prefixes="ext"
 >

 <xsl:variable name="vDoc" select="/"/>
 <xsl:variable name="vNumCols" select="4"/>

 <xsl:variable name="vLower"
  select="'abcdefghijklmnopqrstuvwxyz'"
  />

 <xsl:variable name="vUpper"
  select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"
  />

  <xsl:key name="rows-by-FirstLetter" match="Row"
  use="translate(substring(@Title,1,1),
                 'abcdefghijklmnopqrstuvwxyz',
                 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />

  <xsl:variable name="vrtfStartLetters">
    <xsl:for-each select=
    "/*/*/Row
           [count(.
               |
                key('rows-by-FirstLetter',
                   translate(substring(@Title,1,1),
                             $vLower,
                             $vUpper)
                  )[1]
                  )
              = 1
           ]">

         <startLetter>
           <xsl:value-of select=
             "translate(substring(@Title,1,1),
                      $vLower,
                      $vUpper)"/>
         </startLetter>
      </xsl:for-each>
    </xsl:variable>

    <xsl:variable name="vStartLetters" select=
      "ext:node-set($vrtfStartLetters)"/>

  <xsl:template match="Rows">
    <table>
      <xsl:apply-templates select=
       "$vStartLetters/*[position() mod $vNumCols = 1]">
        <xsl:with-param name="pDoc" select="$vDoc"/>
        <xsl:with-param name="pNumCols" select="$vNumCols"/>
      </xsl:apply-templates>
    </table>
  </xsl:template>

  <xsl:template match="startLetter">
    <xsl:param name="pDoc"/>
    <xsl:param name="pNumCols" select="10"/>
    <tr>
      <xsl:apply-templates mode="copy" select=
      ". | following-sibling::*
               [not(position() >= $pNumCols)]">
         <xsl:with-param name="pDoc" select="$pDoc"/>
         <xsl:sort/>

      </xsl:apply-templates>
    </tr>
  </xsl:template>

  <xsl:template match="startLetter" mode="copy">
    <xsl:param name="pDoc"/>

    <xsl:variable name="pThis" select="."/>

    <td>
      <xsl:value-of select="."/>
      <br />
      <table>
     <xsl:for-each select="$pDoc">
      <xsl:for-each select="key('rows-by-FirstLetter', $pThis)">
        <tr><td><xsl:value-of select="@Title"/></td></tr>
      </xsl:for-each>
     </xsl:for-each>
    </table>
    </td>
  </xsl:template>
</xsl:stylesheet>

when applied on this XML document:

<dsQueryResponse>
  <Rows>
    <Row Title="Agenda" />
    <Row Title="Accrual" />
    <Row Title="Ads" />
    <Row Title="Averages" />
    <Row Title="Bindings" />
    <Row Title="Budget" />
    <Row Title="Cars" />
    <Row Title="Categories" />
    <Row Title="Costs" />
    <Row Title="Policy" />
    <Row Title="Politics" />
    <Row Title="Reevaluations" />
    <Row Title="Report" />
  </Rows>
</dsQueryResponse>

produces the wanted result:

<table>
  <tr>
    <td>A
      <br/>
      <table>
        <tr>
          <td>Agenda</td>
        </tr>
        <tr>
          <td>Accrual</td>
        </tr>
        <tr>
          <td>Ads</td>
        </tr>
        <tr>
          <td>Averages</td>
        </tr>
      </table>
    </td>
    <td>B
      <br/>
      <table>
        <tr>
          <td>Bindings</td>
        </tr>
        <tr>
          <td>Budget</td>
        </tr>
      </table>
    </td>
    <td>C
      <br/>
      <table>
        <tr>
          <td>Cars</td>
        </tr>
        <tr>
          <td>Categories</td>
        </tr>
        <tr>
          <td>Costs</td>
        </tr>
      </table>
    </td>
    <td>P
      <br/>
      <table>
        <tr>
          <td>Policy</td>
        </tr>
        <tr>
          <td>Politics</td>
        </tr>
      </table>
    </td>
  </tr>
  <tr>
    <td>R
      <br/>
      <table>
        <tr>
          <td>Reevaluations</td>
        </tr>
        <tr>
          <td>Report</td>
        </tr>
      </table>
    </td>
  </tr>
</table>

Do note three things:

  • We are using the (exslt) ext:node-set() extension function to convert an intermediate result from RTF (Result-Tree Fragment) to a temporary tree.

  • The <xsl:for-each select="$pDoc"> necessary to make the original XML document the current XML document again, so that the key() function will use the index made for this document and not for the temporary tree.

  • Each start letter that must start a new row of (4) starting letters is processed in a special template, in which the <tr> is produced. Then this and the remaining (3) starting letters in the row are processed within the body of the <tr> in "copy" mode, just creating a <td> each.

Here we have covered and demonstrated a few advanced XSLT techniques:

  • Using the mod operator for grouping by quantity.
  • Using the key() function for a different document than the current one.
  • Modes
  • Converting an RTF into a temporary tree

Enjoy :)

Dimitre Novatchev
+1, again. :) You can spare yourself many headaches if you don't use "<pre>" for code formatting, but select the section in question and press the tilde ~ key, which is a shortcut for the "format as code" button.
Tomalak
(Replacing "<" with "<" will then be unnecessary as well.)
Tomalak
Wow... I will write it down somewhere not to forget.
Dimitre Novatchev
A: 

Here is a second solution, which doesn't require any extension functions. Do note that it is not recursive and may be more efficient than a recursive one.

This transformation:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:my="my:namespace"
 exclude-result-prefixes="my"
 >
 <xsl:output method="html"/>

  <my:alpha>
    <l>A</l><l>B</l><l>C</l><l>D</l><l>E</l>
    <l>F</l><l>G</l><l>H</l><l>I</l><l>J</l>
    <l>K</l><l>L</l><l>M</l><l>N</l><l>O</l>
    <l>P</l><l>Q</l><l>R</l><l>S</l><l>T</l>
    <l>U</l><l>V</l><l>W</l><l>X</l><l>Y</l>
    <l>Z</l>
  </my:alpha>
    <xsl:variable name="vDoc" select="/"/>
    <xsl:variable name="vNumCols" select="4"/>
    <xsl:variable name="vLower"  select="'abcdefghijklmnopqrstuvwxyz'"  />
    <xsl:variable name="vUpper"  select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"  />

    <xsl:key name="rows-by-FirstLetter" match="Row"
      use="translate(substring(@Title,1,1),
                     'abcdefghijklmnopqrstuvwxyz',
                     'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />

  <xsl:variable name="vStartingLetters">
    <xsl:for-each select=
        "/*/*/Row
           [generate-id()
           =
            generate-id(key('rows-by-FirstLetter',
                             translate(substring(@Title,1,1),
                                                 $vLower,
                                                 $vUpper)
                             )[1]
                         )
            ]">
      <xsl:value-of select=
         "translate(substring(@Title,1,1),
                    $vLower,
                    $vUpper)"/>
    </xsl:for-each>
  </xsl:variable>

  <xsl:variable name="vMyLetters" select=
   "document('')/*/my:alpha/l[contains($vStartingLetters,.)]"
   />

    <xsl:template match="Rows">
     <table>
       <xsl:for-each select=
         "$vMyLetters[position() mod $vNumCols = 1]">
         <xsl:variable name="vPos"
            select="(position()-1)*$vNumCols+1"/>

           <tr>
              <xsl:apply-templates select=
               "$vMyLetters[position() >= $vPos
                           and
                            not(position() > $vPos+$vNumCols -1)
                           ]">
      <xsl:with-param name="pDoc" select="$vDoc"/>
              </xsl:apply-templates>
           </tr>
       </xsl:for-each>
     </table>
    </xsl:template>

    <xsl:template match="l">
     <xsl:param name="pDoc"/>

     <xsl:variable name="pThis" select="."/>

     <td>
      <xsl:value-of select="."/>
      <br />
      <table>
       <xsl:for-each select="$pDoc">
        <xsl:for-each select=
                            "key('rows-by-FirstLetter', $pThis)">
       <tr>
         <td>

           <xsl:value-of select="@Title"/>
         </td>
       </tr>
        </xsl:for-each>
       </xsl:for-each>
      </table>
     </td>
    </xsl:template>
</xsl:stylesheet>

when applied on the following XML document:

<dsQueryResponse>
    <Rows>
     <Row Title="Agenda" />
     <Row Title="Policy" />
     <Row Title="Policy" />
     <Row Title="Report" />
     <Row Title="Report" />
     <Row Title="Test2" />
     <Row Title="Test1" />
     <Row Title="Boo" />
     <Row Title="Foo" />
    </Rows>
</dsQueryResponse>

produces the wanted result:


<table>
   <tr>
      <td>A<br><table>
            <tr>
               <td>Agenda</td>
            </tr>
         </table>
      </td>
      <td>B<br><table>
            <tr>
               <td>Boo</td>
            </tr>
         </table>
      </td>
      <td>F<br><table>
            <tr>
               <td>Foo</td>
            </tr>
         </table>
      </td>
      <td>P<br><table>
            <tr>
               <td>Policy</td>
            </tr>
            <tr>
               <td>Policy</td>
            </tr>
         </table>
      </td>
   </tr>
   <tr>
      <td>R<br><table>
            <tr>
               <td>Report</td>
            </tr>
            <tr>
               <td>Report</td>
            </tr>
         </table>
      </td>
      <td>T<br><table>
            <tr>
               <td>Test2</td>
            </tr>
            <tr>
               <td>Test1</td>
            </tr>
         </table>
      </td>
   </tr>
</table>

Do note that most of the explanations in my first answer apply also to this solution with the only exception that here we do not use modes.

Dimitre Novatchev
This is very elegant. I should have though of using not a string of letters but a set of actual elements as the alphabet. Next time I will. Most of the intricateness of my solution comes from the fact that a string cannot easily be iterated over. Thanks for posting this.
Tomalak
BTW have you found out what's the matter with your code formatting? Tilde is not the only way, the code format is available as a button as well.
Tomalak
@Tomalak: I see the button now -- its icon is very misleading -- never would have thought this meant "format code". Will try it the next time I answer a question.
Dimitre Novatchev
@Tomalak: You are welcome :) Do let me know whenever you see a challenging problem. You can contact me on Google's mail, I am "dnovatchev".
Dimitre Novatchev
Thanks for the offer, appreciated. :) *makes a google contact* Unfortunately I don't face many XSLT problems at my day job. Stack Overflow is the enjoyable exception.
Tomalak
I find it funny that you managed to make it past your first 1000 points without noticing the "format as code" button. Good exercise in patience, I guess. :-D
Tomalak
Did you test this stylesheet? I just tested it and it doesn't work.
Tawani
@Tawani Yes, it works with 8 different XSLT processors (MSXML3/4/6, .Net XslCompiledTransform, Saxon 6.5.3, XalanJ, XalanC, JD). Either your XSLT processor is buggy, or you changed the code presented in this solution.
Dimitre Novatchev