Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

alternate sorted nodes in XSLT 1.0 without extension function

Tags:

xslt

xslt-1.0

This a very similar question as XSL: Transforming xml into a sorted multicolumn html table

But (unfortunately) there's an extra requirement: it should be XSLT 1.0 without extension functions, i.e. without using the node-set function.

This is my simplified XML:

<demo>
  <config n_columns="3" />
  <messages>
    <msg date="2011-07-06" title="2nd message" />
    <title>message list</title>
    <msg date="2011-07-05" title="4th message" />
    <msg date="2011-07-06" title="3rd message" />
    <msg date="2011-07-07" title="1st message" />
  </messages>
</demo>

Using this stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" />
  <xsl:template match="/">
    <xsl:apply-templates select="demo/messages">
      <xsl:with-param name="n_columns" select="number(/demo/config/@n_columns)" />
    </xsl:apply-templates>
  </xsl:template>
  <xsl:template match="messages">
    <xsl:param name="n_columns" />
    <div>
      <xsl:value-of select="concat(./title, ' (', $n_columns, ' columns)')" />
    </div>
    <table>
      <xsl:variable name="cells" select="msg" />
      <xsl:apply-templates select="$cells[(position() - 1) mod $n_columns = 0]"
        mode="row">
        <xsl:with-param name="n_columns" select="$n_columns" />
        <xsl:with-param name="cells" select="$cells" />
      </xsl:apply-templates>
    </table>
  </xsl:template>
  <xsl:template match="msg" mode="row">
    <xsl:param name="n_columns" />
    <xsl:param name="cells" />
    <xsl:variable name="n_row" select="position()" />
    <xsl:variable name="row_cells"
      select="$cells[position() > ($n_row - 1) * $n_columns][position() &lt;= $n_columns]" />
    <tr>
      <xsl:apply-templates select="$row_cells" mode="cell" />
      <xsl:call-template name="empty-cells">
        <xsl:with-param name="n" select="$n_columns - count($row_cells)" />
      </xsl:call-template>
    </tr>
  </xsl:template>
  <xsl:template match="msg" mode="cell">
    <td>
      <xsl:value-of select="@title" />
    </td>
  </xsl:template>
  <xsl:template name="empty-cells">
    <xsl:param name="n" />
    <xsl:if test="$n > 0">
      <td>
        <xsl:attribute name="colspan">
          <xsl:value-of select="$n" />
        </xsl:attribute>
        <xsl:text>&#xA0;</xsl:text>
      </td>
    </xsl:if>
  </xsl:template>
</xsl:stylesheet>

Produces this HTML fragment as output:

<div>message list (3 columns)</div>
<table>
  <tr>
    <td>2nd message</td>
    <td>4th message</td>
    <td>3rd message</td>
  </tr>
  <tr>
    <td>1st message</td>
    <td colspan="2">&nbsp;</td>
  </tr>
</table>

What is obviously missing is the sort part...

Redefining the "cells" variable as follows is actually what I need:

<xsl:variable name="cells">
  <xsl:for-each select="msg">
    <xsl:sort select="@date" order="descending" />
    <xsl:sort select="@title" />
    <xsl:copy-of select="." />
  </xsl:for-each>
</xsl:variable>

But now I must define another variable to convert the RTF to a nodelist and pass that one to the template I'm applying.

<xsl:variable name="sCells" select="ext:node-set($cells)/*" />

Doing that would produce the following HTML fragment:

<div>message list (3 columns)</div>
<table>
  <tr>
    <td>1st message</td>
    <td>2nd message</td>
    <td>3rd message</td>
  </tr>
  <tr>
    <td>4th message</td>
    <td colspan="2">&nbsp;</td>
  </tr>
</table>

Unfortunately, my XSLT engine (SAP XML toolkit for java) doesn't support this (or a similar) extension function. Thus I'm looking for another solution that doesn't require the node-set extension function.

I spent quite some time reading all kinds of forums etc., but I really can't figure it out. Perhaps someone has a good idea for an alternative approach? tnx!


This is the follow up based on Dimitre's (slightly extended) solution. This XML input

<demo>
  <config n_columns="3" />
  <messages>
    <msg date="2011-07-06" title="2nd message" />
    <title>message list</title>
    <msg date="2011-07-05" title="4th message" />
    <msg date="2011-07-06" title="3rd message" />
    <msg date="2011-07-07" title="1st message" />
    <msg date="2011-07-05" title="5th message" />
    <msg date="2011-07-05" title="7th message" />
    <msg date="2011-07-05" title="6th message" />
  </messages>
</demo>

combined with this XSLT stylesheet

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="html" />

  <xsl:variable name="vNumCols" select="/*/config/@n_columns" />
  <xsl:variable name="vCells" select="/*/messages/msg" />
  <xsl:variable name="vNumCells" select="count($vCells)" />
  <xsl:variable name="vNumRows" select="ceiling($vNumCells div $vNumCols)" />
  <xsl:variable name="vIndexPatternLength"
    select="string-length(concat('', $vNumCells))" />
  <xsl:variable name="vIndexPattern">
    <xsl:call-template name="padding">
      <xsl:with-param name="length" select="$vIndexPatternLength" />
      <xsl:with-param name="chars" select="'0'" />
    </xsl:call-template>
  </xsl:variable>
  <xsl:variable name="vSortedIndex">
    <xsl:for-each select="$vCells">
      <xsl:sort select="@date" order="descending" />
      <xsl:sort select="@title" />
      <xsl:value-of
        select="format-number(count(preceding-sibling::msg) + 1,
          $vIndexPattern)" />
    </xsl:for-each>
  </xsl:variable>

  <xsl:template match="/">
    <xsl:apply-templates select="demo/messages" />
  </xsl:template>
  <xsl:template match="messages">
    <table>
      <xsl:for-each select="$vCells[not(position() > $vNumRows)]">
        <xsl:variable name="vRow" select="position()" />
        <tr>
          <xsl:for-each select="$vCells[not(position() > $vNumCols)]">
            <xsl:variable name="vCol" select="position()" />
            <xsl:variable name="vCell"
              select="($vRow - 1) * $vNumCols + $vCol" />
            <xsl:variable name="vIndex"
              select="substring($vSortedIndex,
                ($vCell - 1) * $vIndexPatternLength + 1,
                $vIndexPatternLength)" />
            <xsl:variable name="vMessage"
              select="$vCells[position() = $vIndex]" />
            <xsl:choose>
              <xsl:when test="$vMessage">
                <xsl:apply-templates select="$vMessage"
                  mode="cell" />
              </xsl:when>
              <xsl:otherwise>
                <xsl:call-template name="empty-cell" />
              </xsl:otherwise>
            </xsl:choose>
          </xsl:for-each>
        </tr>
      </xsl:for-each>
    </table>
  </xsl:template>

  <xsl:template match="msg" mode="cell">
    <td>
      <xsl:apply-templates select="." />
    </td>
  </xsl:template>
  <xsl:template match="msg">
    <xsl:value-of select="concat(@date, ' : ', @title)" />
  </xsl:template>

  <xsl:template name="empty-cell">
    <td>
      <xsl:text>&#xA0;</xsl:text>
    </td>
  </xsl:template>

  <!-- http://www.exslt.org/str/functions/padding/ -->
  <xsl:template name="padding">
    <xsl:param name="length" select="0" />
    <xsl:param name="chars" select="' '" />
    <xsl:choose>
      <xsl:when test="not($length) or not($chars)" />
      <xsl:otherwise>
        <xsl:variable name="string"
          select="concat($chars, $chars, $chars, $chars, $chars, 
                         $chars, $chars, $chars, $chars, $chars)" />
        <xsl:choose>
          <xsl:when test="string-length($string) >= $length">
            <xsl:value-of select="substring($string, 1, $length)" />
          </xsl:when>
          <xsl:otherwise>
            <xsl:call-template name="padding">
              <xsl:with-param name="length" select="$length" />
              <xsl:with-param name="chars" select="$string" />
            </xsl:call-template>
          </xsl:otherwise>
        </xsl:choose>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>
</xsl:stylesheet>

produces this HTML output

<table>
  <tr>
    <td>2011-07-07 : 1st message</td>
    <td>2011-07-06 : 2nd message</td>
    <td>2011-07-06 : 3rd message</td>
  </tr>
  <tr>
    <td>2011-07-05 : 4th message</td>
    <td>2011-07-05 : 5th message</td>
    <td>2011-07-05 : 6th message</td>
  </tr>
  <tr>
    <td>2011-07-05 : 7th message</td>
    <td>&nbsp;</td>
    <td>&nbsp;</td>
  </tr>
</table>

Thanks Dimitre!

like image 672
Ziggy Avatar asked Jul 06 '11 12:07

Ziggy


1 Answers

It is difficult, but not impossible to perform the required processing in XSLT 1.0 without using any extension functions:

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:strip-space elements="*"/>

 <xsl:variable name="vNumCols"
      select="/*/config/@n_columns"/>

 <xsl:variable name="vItems" select="/*/messages/msg"/>

 <xsl:variable name="vNumItems" select="count($vItems)"/>

 <xsl:variable name="vNumRows" select=
   "ceiling($vNumItems div $vNumCols)"/>

 <xsl:variable name="vsortedInds">
  <xsl:for-each select="$vItems">
   <xsl:sort select="@date" order="descending"/>

   <xsl:value-of select=
   "format-number(count(preceding-sibling::msg)+1,
                  '0000'
                  )
   "/>
  </xsl:for-each>
 </xsl:variable>

 <xsl:template match="/">
  <table>
   <xsl:for-each select=
   "$vItems[not(position() > $vNumRows)]">
     <tr>
       <xsl:variable name="vRow" select="position()"/>

       <xsl:for-each select="$vItems[not(position() > $vNumCols)]">
          <xsl:variable name="vcurIndIndex" select=
           "($vRow -1)*$vNumCols + position()"/>
          <xsl:variable name="vcurInd" select=
          "substring($vsortedInds, 4*($vcurIndIndex -1) +1, 4)"/>

          <xsl:variable name="vcurItem" select="$vItems[position()=$vcurInd]"/>
          <xsl:if test="$vcurItem">
           <td>
            <xsl:value-of select="$vcurItem/@title"/>
           </td>
          </xsl:if>
       </xsl:for-each>
     </tr>
   </xsl:for-each>
  </table>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the provided XML document:

<demo>
    <config n_columns="3" />
    <messages>
        <msg date="2011-07-06" title="2nd message" />
        <title>message list</title>
        <msg date="2011-07-05" title="4th message" />
        <msg date="2011-07-06" title="3rd message" />
        <msg date="2011-07-07" title="1st message" />
    </messages>
</demo>

the significant part of the desired output is produced (I am leaving the rest as an exercise to the reader :) ):

<table>
   <tr>
      <td>1st message</td>
      <td>2nd message</td>
      <td>3rd message</td>
   </tr>
   <tr>
      <td>4th message</td>
   </tr>
</table>

Explanation:

  1. In order to avoid having to convert an RTF to a node-set, we are using a string of the indexes of the sorted elements. Every index occupies four characters (left padded with zeroes when necessary). Then we are using these indexes in populating the rows of the table.

  2. In order to avoid resursion, we are using the Piez method of iteration through N non-node items.

Do note: This solution assumes that the table will not contain more than 9999 cells. If more cells are expected, you can easily change the code, for example:

Replace:

format-number(count(preceding-sibling::msg)+1,
                      '0000'
                    )

with:

format-number(count(preceding-sibling::msg)+1,
                      '00000'
                    )

And replace:

          <xsl:variable name="vcurInd" select=
          "substring($vsortedInds, 4*($vcurIndIndex -1) +1, 4)"/>

with

          <xsl:variable name="vcurInd" select=
          "substring($vsortedInds, 5*($vcurIndIndex -1) +1, 5)"/>
like image 52
Dimitre Novatchev Avatar answered Nov 19 '22 21:11

Dimitre Novatchev