RE: [xsl]: XSL processes XML incorrectly when uneven number of values returned in array elements

Subject: RE: [xsl]: XSL processes XML incorrectly when uneven number of values returned in array elements
From: cknell@xxxxxxxxxx
Date: Fri, 10 Mar 2006 09:23:46 -0500
I think I've solved your problem. I used Saxon 8.7J to process the XSLT.

I cut down your XML so that I could work only on the part that was presenting the challenge. I retained the orginal root element, but excised all other elements but <free-xml> and its children. When you adapt my solution to the original document you will want to pay attention to the XPath expressions to insure that you point to the correct elements.

The entire stylesheet is posted at the end of this message, but I will highlight sections of it here for exposition.

The first problem we had to solve was how to handle the irregular and unknown number of rows to be produced. Since all variables in XSLT are static, the correct approach was to call a named template recursively.

First I calculate the maximum number of rows by creating a variable that contains an XML fragment holding the set of <optionListx> elements sorted in descending order by the count of the number of <option> children.:

  <xsl:template match="free-xml">

    <xsl:variable name="list-with-max-option-children">
      <xsl:for-each select="*">
        <xsl:sort select="count(option)" data-type="number" order="descending" />
        <xsl:copy-of select="." />
      </xsl:for-each>
    </xsl:variable>
    ... The balance of the template goes here ...
  </xsl:template >

Then, by taking the count of the <option> children in the first <optionListx> element in the sorted fragment, I know the maximum number of rows to be produced and we begin the recursive calls to the template that will produce the rows of your table.

  <xsl:template match="free-xml">

    <xsl:variable name="list-with-max-option-children">
      <xsl:for-each select="*">
        <xsl:sort select="count(option)" data-type="number" order="descending" />
        <xsl:copy-of select="." />
      </xsl:for-each>
    </xsl:variable>
    
    <table>
      <xsl:call-template name="table-row">
        <xsl:with-param name="max-rows" select="count($list-with-max-option-children/*[1]/option)" />
        <xsl:with-param name="current-row" select="1" />
      </xsl:call-template>
    </table>
  </xsl:template>

Note that we pass two parameters, "max-rows" which sets the stopping point for the recursive calls to the template named "table-rows", and "current-row" that selects the position of the set of <option> elements whose values will be placed in the <td> elements in the row.

Most of the work is done in the template named "table-row":

<xsl:template name="table-row">
  <xsl:param name="max-rows" />
  <xsl:param name="current-row" />
    
       ... The first thing the template does is check to see if the maximum number of rows has been reached, it aborts if this is true. ...

  <xsl:if test="$current-row &lt;= $max-rows">
    <tr>
      <xsl:choose>
        <xsl:when test="/enquiry-data/free-xml/*[1]/option[$current-row]">
          <td><xsl:value-of select="/enquiry-data/free-xml/*[1]/option[$current-row]" /></td>
        </xsl:when>
        <xsl:otherwise><td /></xsl:otherwise>
      </xsl:choose>

.. Most of the template looks like this. There are known to be exactly seven columns (which translates to seven <td> elements in each row, so the bulk of the template consists of <xsl:choose> elements like this, one for each column. 

 The value in the first set of square brackets here, "*[1]/option[$current-row]" increments for each <xsl:choose> so you get values from 1 through 7. 

The test looks to see if there is an <option> element at that position, and if so, it places its value in the <td>, otherwise an empty <td /> is emitted. 

After processing all <xsl:choose> statments we make a recursive call to the same template. ...

 </tr>
      <xsl:call-template name="table-row">
        <xsl:with-param name="max-rows" select="$max-rows" />
        <xsl:with-param name="current-row" select="$current-row + 1" />
      </xsl:call-template>

    ... The trick here is to add one to the value of $current-row and pass it along to the next call of the template. Although this may look like we are changing the value of a variable (not permitted in XSLT), we really aren't. We are creating a new variable with the same name in a different context.

I've been looking for a new job this week so I've had plenty of time on my hands to work your problem. Not everyone can spend this kind of time on a challenge, so the next time you have one, you may or may not get this much attention. Good luck.

The complete stylesheet:
================
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform";>
  <xsl:output method="xml" indent="yes" />
  <xsl:strip-space elements="*" />

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

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

  <xsl:template match="free-xml">

    <xsl:variable name="list-with-max-option-children">
      <xsl:for-each select="*">
        <xsl:sort select="count(option)" data-type="number" order="descending" />
        <xsl:copy-of select="." />
      </xsl:for-each>
    </xsl:variable>
    
    <table>
      <xsl:call-template name="table-row">
        <xsl:with-param name="max-rows" select="count($list-with-max-option-children/*[1]/option)" />
        <xsl:with-param name="current-row" select="1" />
      </xsl:call-template>
    </table>
  </xsl:template>
  
  <xsl:template name="table-row">
    <xsl:param name="max-rows" />
    <xsl:param name="current-row" />
    
    <xsl:if test="$current-row &lt;= $max-rows">
      <tr>
        <xsl:choose>
          <xsl:when test="/enquiry-data/free-xml/*[1]/option[$current-row]">
            <td><xsl:value-of select="/enquiry-data/free-xml/*[1]/option[$current-row]" /></td>
          </xsl:when>
          <xsl:otherwise><td /></xsl:otherwise>
        </xsl:choose>
        <xsl:choose>
          <xsl:when test="/free-xml/*[2]/option[$current-row]">
            <td><xsl:value-of select="/enquiry-data/free-xml/*[2]/option[$current-row]" /></td>
          </xsl:when>
          <xsl:otherwise><td /></xsl:otherwise>
        </xsl:choose>
        <xsl:choose>
          <xsl:when test="/enquiry-data/free-xml/*[3]/option[$current-row]">
            <td><xsl:value-of select="/enquiry-data/free-xml/*[3]/option[$current-row]" /></td>
          </xsl:when>
          <xsl:otherwise><td /></xsl:otherwise>
        </xsl:choose>
        <xsl:choose>
          <xsl:when test="/enquiry-data/free-xml/*[4]/option[$current-row]">
            <td><xsl:value-of select="/enquiry-data/free-xml/*[4]/option[$current-row]" /></td>
          </xsl:when>
          <xsl:otherwise><td /></xsl:otherwise>
        </xsl:choose>
        <xsl:choose>
          <xsl:when test="/enquiry-data/free-xml/*[5]/option[$current-row]">
            <td><xsl:value-of select="/enquiry-data/free-xml/*[5]/option[$current-row]" /></td>
          </xsl:when>
          <xsl:otherwise><td /></xsl:otherwise>
        </xsl:choose>
        <xsl:choose>
          <xsl:when test="/enquiry-data/free-xml/*[6]/option[$current-row]">
            <td><xsl:value-of select="/enquiry-data/free-xml/*[6]/option[$current-row]" /></td>
          </xsl:when>
          <xsl:otherwise><td /></xsl:otherwise>
        </xsl:choose>
        <xsl:choose>
          <xsl:when test="/enquiry-data/free-xml/*[7]/option[$current-row]">
            <td><xsl:value-of select="/enquiry-data/free-xml/*[7]/option[$current-row]" /></td>
          </xsl:when>
          <xsl:otherwise><td /></xsl:otherwise>
        </xsl:choose>
      </tr>
      <xsl:call-template name="table-row">
        <xsl:with-param name="max-rows" select="$max-rows" />
        <xsl:with-param name="current-row" select="$current-row + 1" />
      </xsl:call-template>
    </xsl:if>
  </xsl:template>
  </xsl:stylesheet>

-- 
Charles Knell
cknell@xxxxxxxxxx - email

Current Thread