tags:

views:

192

answers:

4
+1  Q: 

XPath 1 query

Hi, I posted a similar question and I got a very useful reply. Now the question is a little different, so I post it. I specify it is an XPath 1 related question. This is the content of my XML file:

<?xml version="1.0" encoding="ISO-8859-1"?>
<mainNode>
    <subNode>
     <h hm="08:45">
      <store id="1563">Open</store>
     </h>
     <h hm="13:00">
      <store id="1045">Open</store>
      <store id="763">Open</store>
      <store id="1047">Open</store>
     </h>
     <h hm="16:30">
      <store id="1045">Open</store>
      <store id="763">Open</store>
      <store id="1047">Open</store>
     </h>
     <h hm="20:00">
      <store id="1045">Open</store>
      <store id="1043">Open</store>
      <store id="1052">Open</store>
     </h>
     <h hm="22:00">
         <store id="763">Open</store>
     <store id="1052">Open</store>
     </h>
    </subNode>
</mainNode>

My program gets the current time: if I get 12.40, I must retrieve all the stores id of the next h hm (13.00): this issue has been solved.

After retrieving the data, with a second XPath query, I must get until when, during the current day (of which the XML file is a representation), a single store will be open. So, imagine the store is the store identified with the id=1045 and now it's 12.40 in the morning. This store will close at 20.00 because it is found in the h hm=13.00 subnode, in the h hm=16.30 subnode and in the h hm=20.00 subnode. So, I must get that 20.00.

Case 2: it's 12.40 and I must know when 763 will close. It will close at 16.30, no matter it is included in the last node (h hm=22.00). So, I must get that 16.30.

Is this possible?

Thanks in advance.

+1  A: 

I'll just repeat the last part of my answer to you in that last question you refer to.

It would more pragmatic to load the XML into some data structures that are more conducive to your requirements. This secondary question just re-inforces the sensibleness of that advice.

AnthonyWJones
couldn't agree more
Nathan
I'd +1 this but I ran out of votes
annakata
@AnthonyWJones: there are reasons not to do as you suggest. That is: what you suggest is the last possible thing I can do. I do not say it is not the best, but the best is what is better for the project you are working on.
A: 

If the store ID you're looking for is in $store, this will get the last h/@hm which contains a that ID and is followed by an h which does not contain it or the last h/@hm which contains that store ID (for stores which close in the last h/@hm).

//h[not(store/@id=$store)]/preceding-sibling::h[store/@id = $store][1]/@hm | //h[store/@id=$store][last()]/@hm

To test in XSLT with your example XML:

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
    <xsl:variable name="store" select="763"/>

    <xsl:template match="/">
        <closes>
            <xsl:value-of select="//h[not(store/@id=$store)]/preceding-sibling::h[store/@id = $store][1]/@hm | //h[store/@id=$store][last()]/@hm"/>
        </closes>
    </xsl:template>
</xsl:stylesheet>

Prints <closes>16:30</closes>. If you change $store to 1052, it prints <closes>22:00</closes> instead.

Ben Blank
Thanks for taking the time to answear. One question (from here I can't test, I must wait Monday): what if 763 is in the last node (so that it is not followed by anything)?
*fiddle*, *fiddle* Why, it does the Right Thing, of course! ;-)
Ben Blank
+1  A: 

Here is how such an XPatch expression can be constructed:

The following XPath expression selects the wanted result

($vOpen[not(count(following-sibling::h[1] | $vOpen) 
          = 
           count($vOpen))
       ][1]/@hm
|

  $vOpen[last()]/@hm

 )

  [1]

where $vOpen

is defined as:

$vge1240[store/@id=$vId]

and $vge1240 is defined as:

/*/*/h[translate(@hm,':','') >= 1240]

and $vId is defined as:

1045

or

763

The above variables may be defined and referenced within an XSLT stylesheet or, if XPath is embedded in another host, then each variable reference must be substituted with the right-hand-side of the variable definition. In this case the complete XPath expression will be:

   (/*/*/h[translate(@hm,':','') >= 1240][store/@id=763]
      [not(count(following-sibling::h[1]
                |             
                 /*/*/h[translate(@hm,':','') >= 1240][store/@id=763]
                 )     
          =       
           count(/*/*/h[translate(@hm,':','') >= 1240][store/@id=763])) 
          ]  
          [1]
            /@hm


     |
        /*/*/h[translate(@hm,':','') >= 1240][store/@id=763]
           [last()]
                   /@hm 
       )

        [1]

Explanation:

($vOpen[not(count(following-sibling::h[1] | $vOpen) 
          = 
           count($vOpen))
       ][1]/@hm
|

  $vOpen[last()]/@hm

 )

  [1]

means the following:

  1. From all entries in the "Open" hours that contain the id (763)

  2. Take those, whose immediate following sibling does not belong to that set (closed or not containing 763)

  3. From those take the first one.

  4. Take the first one (in document order) from the node selected in step 3. above and the last element in $vOpen. This will select the last entry in the "Open" hours if all entries in it contain the given Id.

Here we use essentially the Kayesian method for intersection of two nodesets $ns1 and $ns2:

$ns1[count(. | $ns2) = count($ns2)]

Dimitre Novatchev
Hi, Dimitre, only a note: the two-queries design I explained in the first post should be preserved. I only need the second query.
@vyger The XPath expression in my answer is a solution to your second problem (it also solves the first problem and this is not a shortcoming :) )
Dimitre Novatchev
A: 

I think this works. It finds the last consecutive occurrence (after the start time) of store/@id where @id = $id, which is what I believe you were looking for.

<xsl:variable name="id" select="'1045'"/>
<xsl:variable name="st" select="translate('12:40',':','')"/>

...

((//h[translate(@hm,':','') >= $st])[position() = count(preceding-sibling::*[store/@id=$id])+1 and store/@id=$id])[last()]/@hm

Explanation: First, select all elements that share a start time >= to the provided start. Then, within those results, select the @hm attribute of the last element which has a position equal to the number of preceding elements that have the requested store/@id tag within them.

The one limitation in this short version is that it'll fail if the store id does not occur within the the first element after the start time. The one below fixes that limitation by starting with the first after the start time that contains the proper store/@id, but it's a bit messier:

<xsl:variable name="id" select="'1045'"/>
<xsl:variable name="st" select="translate('12:40',':','')"/>

...

((//h[translate(@hm,':','') >= $st and position() >= count(//h[not(preceding-sibling::*[store/@id=$id])])])[position() = count(preceding-sibling::*[store/@id=$id])+1 and store/@id=$id])[last()]/@hm
cmptrgeekken