SUMMARY: Re: Sorting on a variable

Subject: SUMMARY: Re: Sorting on a variable
From: "John E. Simpson" <simpson@xxxxxxxxxxx>
Date: Mon, 16 Oct 2000 19:37:36 -0400
Thanks to input from Mike Kay here on the list, and off-list from Jeni Tennison, I solved my problem of sorting on a "variable." (This isn't quite what I needed to do, as I'll explain in a moment.) Turns out I wasn't far off the mark after all, although I had some problems with copying....

The general solution I'm using is to clone the source tree, adding to it for each product a new child element containing the calculated value (USD-equivalent currency figures, in this case). This clone (an RTF, converted to a node-set using your favorite processor's node-set() extension function) then gets sorted on the created element. Works like a champ. (Sample code below.)

*However*, I discovered a VERY BIG "GOTCHA" to be aware of if you use this technique yourself. That is that the extra element created in the RTF is probably not going to be in the HTML namespace. For instance, I called this element <usd_equiv>, filled it (so I thought) with the desired results, and tried to place it in the result tree. This worked only as long as I did not include the HTML 4.0 namespace declaration for the result tree (i.e., xmlns="http://www.w3.org/TR/REC-html40";). With that namespace declaration, there was no <usd_equiv> element to be displayed (or if there was, it was always empty), let alone sorted on. This was true with both Saxon 5.51 and MSXML Sept '00 release.

Bearing that in mind, then, here's a solution which does what I wanted.

Again, the sample source document looks like this (currency values jiggered a bit to make the sort effect more obvious):
<products>
  <product prodID="A1234">
    <name>First prod</name>
    <price curr="USD">29.95</price>
  </product>
  <product prodID="A5678">
    <name>Second prod</name>
    <price curr="GBP">115.95</price>
  </product>
  <product prodID="A9012">
    <name>Third prod</name>
    <price curr="EU">29.95</price>
  </product>
  <product prodID="A9012">
    <name>Fourth prod</name>
    <price curr="USD">50.00</price>
  </product>
</products>

And here's a stylesheet which sorts the products by their "usd_equiv":


<?xml version="1.0"?>
<!-- Note: Both Saxon and MSXML namespaces declared below.
     Use the one appropriate for your case, and change
     reference to saxon:node-set() below to msxml:node-set()
     if you want to use the MSXML product, e.g. w/IE5.5+.
     Including the HTML 4.0 namespace declaration, i.e.
     xmlns="http://www.w3.org/TR/REC-html40";, makes the
     usd_equiv element empty. So it's not included below. :) -->
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform";
  xmlns:saxon="http://icl.com/saxon";
  xmlns:msxml="urn:schemas-microsoft-com:xslt">

<xsl:template match="/">
  <html>
    <head><title>Sorting an RTF</title></head>
    <body>
      <xsl:apply-templates/>
    </body>
  </html>
</xsl:template>

<!-- Template rule for root element <products> -->
<xsl:template match="products">
  <!-- This variable will hold the RTF, including the
       usd_equiv element for each product -->
  <xsl:variable name="prods_with_usd">
    <xsl:apply-templates select="product" mode="calc_usd" />
  </xsl:variable>
  <table border="1">
    <tr>
      <th>Name/Version</th>
      <th>Price / Curr</th>
      <th>Price (USD)</th>
    </tr>
    <!-- Note that the apply-templates doesn't select the
         <product> children, which would be "conventional," but
         the RTF (converted to node-set) created by the above
         variable. If you're using the MSXML processor, remember
         to change "saxon:" to "msxml:". -->
    <xsl:apply-templates select="saxon:node-set($prods_with_usd)/product">
      <xsl:sort select="usd_equiv" data-type="number" />
    </xsl:apply-templates>
  </table>
</xsl:template>

<!-- When the mode is "calc_usd" (as in apply-templates within
     the xsl:variable above which creates the prods_with_usd RTF),
     copy the product node and its attributes, and add a
     <usd_equiv> child for each <product> element -->
<xsl:template match="product" mode="calc_usd">
  <xsl:copy>
    <xsl:copy-of select="@*" />
    <xsl:copy-of select="*" />
    <!-- <xsl:element> can be replaced, if you want, with a simple
         literal result element, i.e. <usd_equiv>. In either case, the
         element so created isn't in the HTML namespace, which is
         apparently why adding the HTML 4.0 namespace declaration
         makes this <usd_equiv> element "disappear." -->
    <xsl:element name="usd_equiv">
      <xsl:choose>
        <xsl:when test="price/@curr='USD'">
          <xsl:value-of select="format-number(price, '#,##0.00')"/>
        </xsl:when>
        <xsl:when test="price/@curr='GBP'">
          <xsl:value-of select="format-number(price * 1.47275, '#,##0.00')"/>
        </xsl:when>
        <xsl:when test="price/@curr='EU'">
          <xsl:value-of select="format-number(price * 0.864379, '#,##0.00')"/>
        </xsl:when>
        <xsl:otherwise>Unknown Currency</xsl:otherwise>
      </xsl:choose>
    </xsl:element>
  </xsl:copy>
</xsl:template>

<!-- When the mode isn't specified, simply create a table row
     for the product in question. -->
<xsl:template match="product">
  <tr>
    <td valign="top"><xsl:value-of select="name"/></td>
    <td align="right">
      <xsl:value-of select="price"/> / <xsl:value-of select="price/@curr"/>
    </td>
    <td align="right"><xsl:value-of select="usd_equiv"/></td>
  </tr>
</xsl:template>

</xsl:stylesheet>

===============================================================
John E. Simpson | "He asked me if I knew what
http://www.flixml.org | time it was. I said, 'Yes, but
XML Q&A: http://www.xml.com | not right now.'" (Steven Wright)



XSL-List info and archive: http://www.mulberrytech.com/xsl/xsl-list



Current Thread