8

I have some XML that is formatted as follows:

<products>
  <product>
    <name>Product 1</name>
    <price>
      <orig>15</orig>
      <offer>10</offer>
    </price>
  </product>
  <product>
    <name>Product 2</name>
    <price>
      <orig>13</orig>
      <offer>12</offer>
    </price>
  </product>
  <product>
    <name>Product 3</name>
    <price>
      <orig>11</orig>
    </price>
  </product>
</products>

I need to sort the products using XSLT 1.0 (in either ascending or descending order) based on their current price. My difficulty lies in the fact that I need to sort on the lower of the two possible price values <orig> and <offer> if they both exist.

For the above example the correct ordering would be:

  • Product 1 (lowest value = 10)
  • Product 3 (lowest value = 11)
  • Product 2 (lowest value = 12)

Any help would be much appreciated, as I can't seem to find a similar question through the search.

Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
MarkS
  • 127
  • 6

3 Answers3

6

(answer updated to include thoughts on both XSLT 1.0 and 2.0)

I. XSLT 1.0:

Note that XSLT 1.0 does not have a built-in equivalent to min(); assuming your parser supports EXSLT, you can make use of its math:min() function to achieve a solution quite similar to the below XSLT 2.0 variant.


II. XSLT 2.0:

Here is a solution that makes use of the XPath 2.0 aggregation function min().

When this XSLT 2.0 solution:

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

  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="products">
    <products>
      <xsl:apply-templates select="product">
        <xsl:sort select="min(price/offer|price/orig)"
          data-type="number" order="ascending" />
      </xsl:apply-templates> 
    </products>
  </xsl:template>

</xsl:stylesheet>

..is applied to the provided XML:

<products>
  <product>
    <name>Product 1</name>
    <price>
      <orig>15</orig>
      <offer>10</offer>
    </price>
  </product>
  <product>
    <name>Product 2</name>
    <price>
      <orig>13</orig>
      <offer>12</offer>
    </price>
  </product>
  <product>
    <name>Product 3</name>
    <price>
      <orig>11</orig>
    </price>
  </product>
</products>

..the wanted result is produced:

<?xml version="1.0" encoding="UTF-8"?>
<products>
   <product>
      <name>Product 1</name>
      <price>
         <orig>15</orig>
         <offer>10</offer>
      </price>
   </product>
   <product>
      <name>Product 3</name>
      <price>
         <orig>11</orig>
      </price>
   </product>
   <product>
      <name>Product 2</name>
      <price>
         <orig>13</orig>
         <offer>12</offer>
      </price>
   </product>
</products>
ABach
  • 3,743
  • 5
  • 25
  • 33
  • @MarkS - I've updated my answer to provide a possible XSLT 1.0 solution. Do you know: does your XSLT parser implement the EXSLT extension functions (or, in particular, the `math` subset of those functions)? – ABach Sep 05 '12 at 22:04
  • It does. Looks like this could be the solution. I'll have a play before I give any feedback. Many Thanks! – MarkS Sep 05 '12 at 22:22
  • All works fine with math:min(price/*). Really helpful answer - thanks again! – MarkS Sep 05 '12 at 22:42
5

An XSLT 1.0 solution that does not require EXSLT:

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

    <xsl:template match="node()|@*">
        <xsl:copy>
            <xsl:apply-templates select="node()|@*"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="products">
        <products>
            <xsl:apply-templates select="product">
                <xsl:sort select="(price/*[not(. > ../*)])[1]"
                    data-type="number" order="ascending" />
            </xsl:apply-templates> 
        </products>
    </xsl:template>

</xsl:stylesheet>
Mads Hansen
  • 63,927
  • 12
  • 112
  • 147
5

I. There is a general and pure XSLT 1.0 solution -- as simple as this:

<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:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="/*">
  <products>
   <xsl:apply-templates select="*">
    <xsl:sort data-type="number" select=
    "price/*[not(../* &lt; .)]"/>
   </xsl:apply-templates>
  </products>
 </xsl:template>
</xsl:stylesheet>

II. If price has other children in addition to offer and orig -- in this case the general solution I. above (as well as the other two answers to this question) doesn't work correctly.

Here is a correct solution for this case:

<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:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="/*">
  <products>
   <xsl:apply-templates select="*">
    <xsl:sort data-type="number" select=
    "sum(price/orig[not(../offer &lt;= .)])
   +
     sum(price/offer[not(../orig &lt; .)])
    "/>
   </xsl:apply-templates>
  </products>
 </xsl:template>
</xsl:stylesheet>

III. If we know that offer never exceeds orig:

<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:template match="node()|@*">
  <xsl:copy>
   <xsl:apply-templates select="node()|@*"/>
  </xsl:copy>
 </xsl:template>

 <xsl:template match="/*">
  <products>
   <xsl:apply-templates select="*">
    <xsl:sort data-type="number" 
         select="price/offer | price/orig[not(../offer)]"/>
   </xsl:apply-templates>
  </products>
 </xsl:template>
</xsl:stylesheet>

IV. Verification:

All three transformations above, when applied to the provided XML document:

<products>
  <product>
    <name>Product 1</name>
    <price>
      <orig>15</orig>
      <offer>10</offer>
    </price>
  </product>
  <product>
    <name>Product 2</name>
    <price>
      <orig>13</orig>
      <offer>12</offer>
    </price>
  </product>
  <product>
    <name>Product 3</name>
    <price>
      <orig>11</orig>
    </price>
  </product>
</products>

produce the wanted, correct result:

<products>
   <product>
      <name>Product 1</name>
      <price>
         <orig>15</orig>
         <offer>10</offer>
      </price>
   </product>
   <product>
      <name>Product 3</name>
      <price>
         <orig>11</orig>
      </price>
   </product>
   <product>
      <name>Product 2</name>
      <price>
         <orig>13</orig>
         <offer>12</offer>
      </price>
   </product>
</products>

Solution II is the only of the three that still produces the correct result when applied on this XML document (added a minAcceptable child to price):

<products>
  <product>
    <name>Product 1</name>
    <price>
      <orig>15</orig>
      <offer>10</offer>
      <minAcceptable>8</minAcceptable>
    </price>
  </product>
  <product>
    <name>Product 2</name>
    <price>
      <orig>13</orig>
      <offer>12</offer>
      <minAcceptable>6</minAcceptable>
    </price>
  </product>
  <product>
    <name>Product 3</name>
    <price>
      <orig>11</orig>
      <minAcceptable>7</minAcceptable>
    </price>
  </product>
</products>

Do note that none of the other answers processes this XML document correctly.

Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431
  • +1 Very elegant - nice work, @DimitreNovatchev. I would argue, however, that Solution II is not appropriate to the question (as it addresses a situation never described by the OP) and therefore should not be used as evidence of inadequacy in other answers. :) – ABach Sep 06 '12 at 23:11
  • @ABach, You are welcome. As for the relevance, both solution 1. and 3. are exactly following the OP's XML document. Solution 2 gives us knowledge what to do in a slightly different situation, when other solution don't work. Knowledge is power, don't you thonk so? – Dimitre Novatchev Sep 07 '12 at 01:32