2

I'm trying to transform some XML output to a rating table (hydrology related) which consists of the hundredths values going across the top and the tenths values going down. The problem I'm running into is that I can't seem to sort for the unique values by using a substring on the ht attributes.

Here is a sample of the XML output:

<env:Envelope>
<env:Header/>
<env:Body>
<RatingTableUpdate>      
  <Gauge xmlns="" type="STORAGE" id="123">
    <Input input="0">
      <Conv ht="19.76" cap="5040" />
      <Conv ht="19.77" cap="5045" />
      <Conv ht="19.78" cap="5051" />
      <Conv ht="19.79" cap="5057" />
      <Conv ht="19.80" cap="5063" />
      <Conv ht="19.81" cap="5069" />
      <Conv ht="19.82" cap="5074" />
      <Conv ht="19.83" cap="5080" />
      <Conv ht="19.84" cap="5086" />
      <Conv ht="19.85" cap="5092" />
      <Conv ht="19.86" cap="5098" />
      <Conv ht="19.87" cap="5104" />
      <Conv ht="19.88" cap="5109" />
      <Conv ht="19.89" cap="5115" />
      <Conv ht="19.90" cap="5121" />
      <Conv ht="19.91" cap="5127" />
      <Conv ht="19.92" cap="5133" />
      <Conv ht="19.93" cap="5138" />
    </Input>
  </Gauge>
</RatingTableUpdate>
</env:Body>
</env:Envelope>

The output should look like this:

GH    0.00  0.01  0.02  0.03  0.04  0.05  0.06  0.07  0.08  0.09
19.7                                      5040  5045  5051  5057  
19.8  5063  5069  5074  5080  5086  5092  5098  5104  5109  5115
19.9  5121  5127  5133  5138

I'm able to get the cap attributes in the correct column, but can't get the GH column (ht attribute with hundredths values dropped) to only display once per row. I get something like this:

GH    0.00  0.01  0.02  0.03  0.04  0.05  0.06  0.07  0.08  0.09
19.7                                      5040  
                                                5045  
                                                      5051  
                                                            5057  
19.8  5063  
            5069  
                  5074  
                        5080  
                              5086  
                                    5092  
                                          5098  
                                                5104  
                                                      5109  
                                                            5115
19.9  5121  
            5127  
                  5133  
                        5138

Here's my section of xsl that formats this section. The xsl:choose seems to work well for the columns. I've tried the Muenchian method but can't get it quite right (having a hard time getting it to work with a substring key).

Is this the best method, or is there another way to get each row to display only once for the given substring (19.7 once for 19.76, 19.77, 19.78, 19.79 values)? The ratings commonly start at non-zero values like the example, so I can't just use the hundredth 0 values to start a group and 9 to end a group.

<xsl:for-each select="env:Envelope/env:Body/RatingTableUpdate/Gauge">

<table width="100%">
<tr class="site">                           
  <td width="150px">Site ID: <u><xsl:value-of select="@id" /></u></td>
  <td>Site Name: <u><xsl:value-of select="@idname" /></u></td>
  <td>Type: <u><xsl:value-of select="@type" /></u></td>
</tr>   
</table>        

<table width="100%">
  <tr class="tbldata">
    <th rowspan="1" class="tbldataheader">GH</th>
    <th rowspan="1" class="tbldataheader">0.00</th>
    <th rowspan="1" class="tbldataheader">0.01</th>
    <th rowspan="1" class="tbldataheader">0.02</th>
    <th rowspan="1" class="tbldataheader">0.03</th>
    <th rowspan="1" class="tbldataheader">0.04</th>
    <th rowspan="1" class="tbldataheader">0.05</th>
    <th rowspan="1" class="tbldataheader">0.06</th>
    <th rowspan="1" class="tbldataheader">0.07</th>
    <th rowspan="1" class="tbldataheader">0.08</th>
    <th rowspan="1" class="tbldataheader">0.09</th>
  </tr>  

  <xsl:for-each select="./Input/Conv">
    <tr class="tbldata">

      <xsl:choose>
        <xsl:when test="substring(@ht,string-length(@ht),1) = 0">
          <td class="ifrtext"><xsl:value-of select="substring(@ht,1,string-length(@ht)-1)" /></td>
        </xsl:when>
        <xsl:otherwise>
          <td class="ifrtext"></td>
        </xsl:otherwise>
      </xsl:choose>
       <xsl:choose>
        <xsl:when test="substring(@ht,string-length(@ht),1) = 0">
          <td class="ifrtext"><xsl:value-of select="@cap" /></td>
        </xsl:when>
        <xsl:otherwise>
          <td class="ifrtext"></td>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:choose>
        <xsl:when test="substring(@ht,string-length(@ht),1) = 1">
          <td class="ifrtext"><xsl:value-of select="@cap" /></td>
        </xsl:when>
        <xsl:otherwise>
          <td class="ifrtext"></td>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:choose>
        <xsl:when test="substring(@ht,string-length(@ht),1) = 2">
          <td class="ifrtext"><xsl:value-of select="@cap" /></td>
        </xsl:when>
        <xsl:otherwise>
          <td class="ifrtext"></td>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:choose>
        <xsl:when test="substring(@ht,string-length(@ht),1) = 3">
          <td class="ifrtext"><xsl:value-of select="@cap" /></td>
        </xsl:when>
        <xsl:otherwise>
          <td class="ifrtext"></td>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:choose>
        <xsl:when test="substring(@ht,string-length(@ht),1) = 4">
          <td class="ifrtext"><xsl:value-of select="@cap" /></td>
        </xsl:when>
        <xsl:otherwise>
          <td class="ifrtext"></td>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:choose>
        <xsl:when test="substring(@ht,string-length(@ht),1) = 5">
          <td class="ifrtext"><xsl:value-of select="@cap" /></td>
        </xsl:when>
        <xsl:otherwise>
          <td class="ifrtext"></td>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:choose>
        <xsl:when test="substring(@ht,string-length(@ht),1) = 6">
          <td class="ifrtext"><xsl:value-of select="@cap" /></td>
        </xsl:when>
        <xsl:otherwise>
          <td class="ifrtext"></td>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:choose>
        <xsl:when test="substring(@ht,string-length(@ht),1) = 7">
          <td class="ifrtext"><xsl:value-of select="@cap" /></td>
        </xsl:when>
        <xsl:otherwise>
          <td class="ifrtext"></td>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:choose>
        <xsl:when test="substring(@ht,string-length(@ht),1) = 8">
          <td class="ifrtext"><xsl:value-of select="@cap" /></td>
        </xsl:when>
        <xsl:otherwise>
          <td class="ifrtext"></td>
        </xsl:otherwise>
      </xsl:choose>
      <xsl:choose>
        <xsl:when test="substring(@ht,string-length(@ht),1) = 9">
          <td class="ifrtext"><xsl:value-of select="@cap" /></td>
        </xsl:when>
        <xsl:otherwise>
          <td class="ifrtext"></td>
        </xsl:otherwise>
      </xsl:choose>
    </tr>   
  </xsl:for-each>
</table>
</xsl:for-each>

Thanks, Matt

mmcpheet
  • 78
  • 5

1 Answers1

2

This transformation:

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

 <xsl:key name="kValByTenths" match="Conv"
      use="substring(@ht, 1, 4)"/>

 <xsl:key name="kCapFromHt" match="@cap"
      use="../@ht"/>

 <xsl:template match="Input">
  <table>
   <xsl:for-each select=
    "Conv[generate-id()
         =
          generate-id(key('kValByTenths',substring(@ht, 1, 4))[1])
          ]">
     <xsl:variable name="vKey" select="substring(@ht, 1, 4)"/>
     <tr>
      <td><xsl:value-of select="$vKey"/></td>

      <xsl:for-each select=
       "(//node() | //namespace::*)[not(position() > 10)]">
        <xsl:variable name="vPos" select="position()"/>
        <td>
         <xsl:value-of select="key('kCapFromHt', concat($vKey,$vPos -1))"/>
        </td>
      </xsl:for-each>
     </tr>
    </xsl:for-each>
   </table>
 </xsl:template>
</xsl:stylesheet>

when applied on the provided XML document (corrected to be made well-formed XML document):

<env:Envelope xmlns:env="some:env">
    <env:Header/>
    <env:Body>
        <RatingTableUpdate>
            <Gauge xmlns="" type="STORAGE" id="123">
                <Input input="0">
                    <Conv ht="19.76" cap="5040" />
                    <Conv ht="19.77" cap="5045" />
                    <Conv ht="19.78" cap="5051" />
                    <Conv ht="19.79" cap="5057" />
                    <Conv ht="19.80" cap="5063" />
                    <Conv ht="19.81" cap="5069" />
                    <Conv ht="19.82" cap="5074" />
                    <Conv ht="19.83" cap="5080" />
                    <Conv ht="19.84" cap="5086" />
                    <Conv ht="19.85" cap="5092" />
                    <Conv ht="19.86" cap="5098" />
                    <Conv ht="19.87" cap="5104" />
                    <Conv ht="19.88" cap="5109" />
                    <Conv ht="19.89" cap="5115" />
                    <Conv ht="19.90" cap="5121" />
                    <Conv ht="19.91" cap="5127" />
                    <Conv ht="19.92" cap="5133" />
                    <Conv ht="19.93" cap="5138" />
                </Input>
            </Gauge>
        </RatingTableUpdate>
    </env:Body>
</env:Envelope>

produces the wanted (without the decorations), correct result:

<table>
   <tr>
      <td>19.7</td>
      <td/>
      <td/>
      <td/>
      <td/>
      <td/>
      <td/>
      <td>5040</td>
      <td>5045</td>
      <td>5051</td>
      <td>5057</td>
   </tr>
   <tr>
      <td>19.8</td>
      <td>5063</td>
      <td>5069</td>
      <td>5074</td>
      <td>5080</td>
      <td>5086</td>
      <td>5092</td>
      <td>5098</td>
      <td>5104</td>
      <td>5109</td>
      <td>5115</td>
   </tr>
   <tr>
      <td>19.9</td>
      <td>5121</td>
      <td>5127</td>
      <td>5133</td>
      <td>5138</td>
      <td/>
      <td/>
      <td/>
      <td/>
      <td/>
      <td/>
   </tr>
</table>

Explanation:

  1. Proper use of the Muenchian grouping method where a Conv element is indexed by the first 4 characters of the string vaue of its ht attribute.

  2. Proper use of the Piez method for non-recursive iteration N times.

Update:

The OP has reported that due to a bug in MSXML3 he cannot get the correct result when using IE 8.

Here is a modified solution that also works OK with MSXML3 / IE 8:

<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:key name="kValByTenths" match="Conv"
      use="substring(@ht, 1, 4)"/>

 <xsl:variable name="vConvs" select="//Conv"/>

 <xsl:template match="Input">
  <table>
   <xsl:for-each select=
    "Conv[generate-id()
         =
          generate-id(key('kValByTenths',substring(@ht, 1, 4))[1])
          ]">
     <xsl:variable name="vKey" select="substring(@ht, 1, 4)"/>
     <tr>
      <td><xsl:value-of select="$vKey"/></td>

      <xsl:for-each select=
       "(//node() | //namespace::*)[not(position() > 10)]">
        <xsl:variable name="vPos" select="position()"/>

        <td>
         <xsl:value-of select=
         "$vConvs[substring-after(@ht, $vKey) = $vPos]/@cap"/>
        </td>
      </xsl:for-each>
     </tr>
    </xsl:for-each>
   </table>
 </xsl:template>
</xsl:stylesheet>
Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
  • Thanks for your quick response, that's impressive! That code puts it in the correct format with the proper number of cells on my output, but every third value isn't showing for me so cap values for ht values of 19.78, 19.82, 19.85, 19.88, and 19.92 are blank/missing. The others show and are in the proper columns. Any idea what would be causing that behavior? – mmcpheet Jul 15 '12 at 21:27
  • @mmcpheet: If you run the transformation from this answer on the XML document from this answer you should get exactly the same result as provided here. If the result is different then either your XSLT processor is incompliant/buggy, or you have committed copy/paste error. If you run this transformation on different XML document that you haven't shown in the question, or if you modify the transformation, then the result can be quite different from what you want it to be. – Dimitre Novatchev Jul 15 '12 at 21:40
  • I copied both exactly as you had them and saved them as an XML file with a header pointing to the XSL file (). I open the XML with IE8 and the same values are missing... Would these methods be IE8 compliant? edited to add: Firefox output looks good. – mmcpheet Jul 15 '12 at 22:06
  • This seems to be a bug in MSXML3 (which IE up to verdion 8 uses) -- all other XSLT processors I have produce the result that is provided in the answer. – Dimitre Novatchev Jul 15 '12 at 22:10
  • OK, I accepted your original answer since it works in Firefox. Any idea which portion of the xsl isn't compliant? – mmcpheet Jul 15 '12 at 22:22
  • @mmcpheet: I will shortly update the answer with a transformation that works OK with MSXML3 -- stay tuned. – Dimitre Novatchev Jul 15 '12 at 22:28
  • Very nice! a quick change from $vPos to $vPos -1 and the solutions works in IE8 and Firefox. – mmcpheet Jul 15 '12 at 22:59