2

I have an xml which contains prices for different dates

<tour id="12314">
  <available date="2012-04-19" price="533" /> 
  <available date="2012-05-09" price="670" /> 
  <available date="2012-05-25" price="600" /> 
  <available date="2012-06-05" price="710" /> 
  <available date="2012-06-08" price="710" /> 
  <available date="2012-06-15" price="710" /> 
  <available date="2012-06-20" price="705" /> 
</tour>

My requirement is to get the nodes which have the cheapest price for each month using XSLT ex: the desired output is:

 <available dt="2012-04-19" price="533" /> 
 <available dt="2012-05-25" price="600" /> 
 <available dt="2012-06-20" price="705" /> 

I started by sorting the available node as below, but I am not sure how to get the nodes grouped by month with the cheapest price

<xsl:for-each select="tour[@id='12314']/available">
    <xsl:sort select="substring(@dt,1,7)"/>
    <xsl:sort select="@price"/>
    <!-- I would like to access the available node which has the cheapest price for each month -->
</xsl:for-each>

Any help will be much appreciated

jack
  • 1,488
  • 7
  • 25
  • 44

2 Answers2

2

XSLT 1.0 Solution using Muenchian method and simple sorting :

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

  <xsl:key name="months" match="available" use="substring(@date, 6, 2)"/>

  <xsl:template match="/tour[@id = '12314']">
    <result>
      <xsl:for-each select="./available[generate-id() = generate-id(key('months',substring(@date, 6, 2) )[1])]">
        <xsl:for-each select="key('months',substring(@date, 6, 2))">
          <xsl:sort select="@price" order="ascending"/>
          <xsl:if test="position() = 1">
            <xsl:copy-of select="current()"/>
          </xsl:if>
        </xsl:for-each>
      </xsl:for-each>
    </result>
  </xsl:template>
</xsl:stylesheet>

When the above transform is applied to this xml :

 <tour id="12314">
  <available date="2012-04-19" price="533" /> 
  <available date="2012-05-09" price="670" /> 
  <available date="2012-05-25" price="600" /> 
  <available date="2012-06-05" price="710" /> 
  <available date="2012-06-08" price="710" /> 
  <available date="2012-06-15" price="710" /> 
  <available date="2012-06-20" price="705" /> 
</tour>

The output is :

<?xml version="1.0" encoding="UTF-8"?>
<result>
   <available date="2012-04-19" price="533"/>
   <available date="2012-05-25" price="600"/>
   <available date="2012-06-20" price="705"/>
</result>

Logic : first I group the available nodes based on a substring of @date attribute and then for each of those unique months I collect all available nodes sort them with ascending order and just print the node with the minimum price which is by definition the 1st node due to the sorting. Hope it helped :)

FailedDev
  • 26,680
  • 9
  • 53
  • 73
  • @_FailedDev: This solution is very nice in case only one of many minimum-price tours is wanted. It seems to me that it wouldn't be possible using this solution and without modifications for linear search to output *all* tours with minimum price. What do you think? – Dimitre Novatchev Oct 11 '11 at 21:08
  • @DimitreNovatchev I based my solution on the OP's attempt since he was using the speicific ID. Of course there could be more general alternatives - as you pointed out :) – FailedDev Oct 12 '11 at 03:29
1

I am offering a total of three alternative solutions each short and simple (no nested <xsl:for-each> and no sorting). If it is possible, I'd recommend using the XSLT 2.0 solution.

I. Two alternative XSLT 1.0 solutions:

1. Without keys:

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

 <xsl:template match="available">
  <xsl:if test=
  "not(@price
      >
       ( preceding-sibling::available
       |
        following-sibling::available
        )
           [substring(@date, 1, 7)
           =
            substring(current()/@date, 1, 7)
           ]
           /@price
       )">
   <xsl:call-template name="identity"/>
  </xsl:if>
 </xsl:template>
</xsl:stylesheet>

2. Using keys:

<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="kDateByMonth" match="available"
          use="substring(@date, 1, 7)"/>

 <xsl:template match=
  "available
      [generate-id()
      =
       generate-id(key('kDateByMonth',
                       substring(@date, 1, 7)
                       )[1]
                   )
      ]
  ">
  <xsl:variable name="vsameMonth" select=
   "key('kDateByMonth',
         substring(@date, 1, 7)
         )
   "/>

  <xsl:copy-of select=
    "$vsameMonth[not(@price > $vsameMonth/@price)][1]
    "/>
 </xsl:template>
</xsl:stylesheet>

when any of the two transformations above is applied to the provided XML document:

the wanted, correct result is produced:

<tour id="12314">
   <available date="2012-04-19" price="533"/>
   <available date="2012-05-25" price="600"/>
   <available date="2012-06-20" price="705"/>
</tour>

Note: In the question it wasn't specified what to output if more than one tour in a month have the same minimum price. The first transformation will output all such tours (and probably will give choice to the reader), while the second transformation outputs only one such tour per month. Both transformations can be modified to implement the other behavior.

II. An XSLT 2.0 solution:

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

 <xsl:template match="/*">
  <xsl:for-each-group select="*"
                       group-by="substring(@date, 1,7)">
    <xsl:copy-of select=
     "current-group()
        [@price
        =
         min(current-group()/@price/number())
         ]
         [1]"/>
  </xsl:for-each-group>
 </xsl:template>
</xsl:stylesheet>
Dimitre Novatchev
  • 240,661
  • 26
  • 293
  • 431