Sarah Vessels
Sarah Vessels

Reputation: 31630

XSLT - select nodes that came after another node

I'm trying to select all nodes that 1) come after a node with a particular property and 2) have a particular property themselves. So if I had the following XML:

<node id="1"><child attr="valueOfInterest"/></node>
<node id="2"><child attr="boringValue"/></node>
...
<node id="3"><child attr="valueOfInterest"/></node>
<node id="4"><child attr="boringValue"/></node>
<node id="5"><child attr="boringValue"/></node>
<node id="6"><child attr="boringValue"/></node>
...

My XSLT traverses through each node tag. At each node, I want it to select all previous nodes that occurred since the most recent node that had a child whose attr was valueOfInterest. So if I were at node #2, I would want an empty node set. If I were at node #6, I would want to select node #'s 4 and 5. I currently have the following XSLT:

<xsl:variable name="prev_vals"
    select="preceding-sibling::node/child[@attr = $someValueICareAbout]/@attr"/>

So this XSLT gets all preceding attr values that are a particular value. How do I only get those preceding attr values that are in nodes that come after the most recent node whose child has a particular attr value (i.e., "valueOfInterest")? The id attribute on node tags is not guaranteed to be increasing, so we can't compare against that.

Edit: I thought these might be of use:

<xsl:variable name="prev_children_of_interest"
    select="preceding-sibling::node/child[@attr != $someValueICareAbout]"/>
<xsl:variable name="mru_child_of_interest"
    select="$prev_children_of_interest[count($prev_children_of_interest)]"/>

So that's all previous child tags with attr=valueOfInterest and then the most recently used (closest to current node) child tag that has the attribute I'm looking for. From mru_child_of_interest we can find the most recently used parent tag, but how do we then look for nodes that come after that tag?

Upvotes: 6

Views: 7710

Answers (4)

cordsen
cordsen

Reputation: 1701

It looks like you want the intersection of two sets. Set 1 is all the nodes after the last valueOfInterest. Set 2 is all the nodes before the current node that do not contain valueOfInterest. For XPath 1.0, the following will give you the intersection (reference found here).

$set1[count($set2|.)=count($set2)]

Given your input, the following XSL demonstrates the nodeset you are looking for

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes"/>
  <xsl:strip-space elements="*"/>
  <xsl:variable name="someValueICareAbout">valueOfInterest</xsl:variable>
  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="node[child/@attr='boringValue']">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()"/>

      <xsl:variable name="set1" select="preceding-sibling::node[child/@attr='valueOfInterest'][1]/following-sibling::node "/>

      <xsl:variable name="set2" select="preceding-sibling::node[child/@attr='boringValue']"/>

      <predecessors>
          <xsl:copy-of select="$set1[count($set2|.)=count($set2)]"/>
      </predecessors>

    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

Here is the output

<xml>
  <node id="1">
    <child attr="valueOfInterest"/>
  </node>
  <node id="2">
    <child attr="boringValue"/>
    <predecessors/>
  </node>
  <node id="3">
    <child attr="valueOfInterest"/>
  </node>
  <node id="4">
    <child attr="boringValue"/>
    <predecessors/>
  </node>
  <node id="5">
    <child attr="boringValue"/>
    <predecessors>
      <node id="4">
        <child attr="boringValue"/>
      </node>
    </predecessors>
  </node>
  <node id="6">
    <child attr="boringValue"/>
    <predecessors>
      <node id="4">
        <child attr="boringValue"/>
      </node>
      <node id="5">
        <child attr="boringValue"/>
      </node>
    </predecessors>
  </node>
</xml>

Note, the reason I use [1] in preceding-sibling::node[child/@attr='valueOfInterest'][1] is because the order of the nodeset is reversed by preceding-sibling see here.

If you have XPath 2.0, you can use the intersect operator

  <predecessors>
    <xsl:copy-of select="$set1 intersect $set2"/>
  </predecessors>

This produces the same result.

Upvotes: 3

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243469

This transformation copies exactly the wanted nodes:

<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[not(child/attr='valueOfInterest')]">

 <xsl:variable name="vFollowing" select=
 "preceding-sibling::node
          [child/@attr='valueOfInterest'][1]
            /following-sibling::node"/>

 <xsl:variable name="vPreceding" select=
  "preceding-sibling::node"/>

  <xsl:copy-of select=
  "$vFollowing[count(. | $vPreceding)
              =
               count($vPreceding)
              ]
  "/>
======================
 </xsl:template>
 <xsl:template match="text()"/>
</xsl:stylesheet>

when applied on this XML document (based on the provided XML fragment and wrapping it in a top element to make it a well-formed XML document):

<t>
    <node id="1">
        <child attr="valueOfInterest"/>
    </node>
    <node id="2">
        <child attr="boringValue"/>
    </node>...
    <node id="3">
        <child attr="valueOfInterest"/>
    </node>
    <node id="4">
        <child attr="boringValue"/>
    </node>
    <node id="5">
        <child attr="boringValue"/>
    </node>
    <node id="6">
        <child attr="boringValue"/>
    </node>...
</t>

the wanted, correct result is produced:

======================

======================
 <node id="2">
   <child attr="boringValue"/>
</node>
======================

======================
 <node id="4">
   <child attr="boringValue"/>
</node>
======================
 <node id="4">
   <child attr="boringValue"/>
</node>
<node id="5">
   <child attr="boringValue"/>
</node>
======================

Explanation:

  1. Here we use the well-known Kayessian formula (discovered by the SO user @Michael Kay) for the intersection of two node-sets $ns1 and $ns2:

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

  2. We simply substitute $vFollowing and $vPreceding for $ns1 and $ns2 in the above formula.

$vFollowing is defined to contain exactly all the following sibling elements namednodeof the nearestnode` that satisfies the condition (to be interesting).

$vPreceding is the set of all node elements that are preceding siblings of the current (matched) node.

.3. Their intersection is exactly the wanted node-set.

Upvotes: 3

Grzegorz Szpetkowski
Grzegorz Szpetkowski

Reputation: 37914

I am not sure if I understand your question correctly, but here is some XSL 1.0 (additional each-nodes attributes are informational only):

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

    <xsl:template match="nodes">
        <xsl:copy>
            <xsl:apply-templates select="node"/>
        </xsl:copy>
    </xsl:template>

    <xsl:template match="node">
        <xsl:variable name="someValueICareAbout">valueOfInterest</xsl:variable>

        <xsl:variable name="recentParticularNode"
            select="preceding-sibling::node[child/@attr = $someValueICareAbout][1]"/>

        <xsl:variable name="recentParticularNodePosition"
            select="count($recentParticularNode/preceding-sibling::node) + 1"/>

        <xsl:variable name="currentPosition" select="position()"/>

        <xsl:if test="child/@attr != $someValueICareAbout">
            <each-nodes id="{@id}" cp="{$currentPosition}" 
                    rpnp="{$recentParticularNodePosition}">
                <xsl:copy-of
                    select="../node[position() &gt; $recentParticularNodePosition
                    and position() &lt; $currentPosition]"/>
            </each-nodes>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

Input XML:

<?xml version="1.0" encoding="UTF-8"?>
<nodes>
    <node id="1"><child attr="valueOfInterest"/></node>
    <node id="2"><child attr="boringValue2"/></node>
    <node id="3"><child attr="valueOfInterest"/></node>
    <node id="4"><child attr="boringValue4"/></node>
    <node id="5"><child attr="boringValue5"/></node>
    <node id="6"><child attr="boringValue6"/></node>
</nodes>

Result XML:

<?xml version="1.0" encoding="UTF-8"?>
<nodes>
   <each-nodes id="2" cp="2" rpnp="1"/>
   <each-nodes id="4" cp="4" rpnp="3"/>
   <each-nodes id="5" cp="5" rpnp="3">
      <node id="4">
         <child attr="boringValue4"/>
      </node>
   </each-nodes>
   <each-nodes id="6" cp="6" rpnp="3">
      <node id="4">
         <child attr="boringValue4"/>
      </node>
      <node id="5">
         <child attr="boringValue5"/>
      </node>
   </each-nodes>
</nodes>

Upvotes: 7

LarsH
LarsH

Reputation: 27995

Here's one way to do it in XSLT 2.0:

<xsl:variable name="prevVOI"
      select="(preceding-sibling::node[child/@attr = 'valueOfInterest'])[last()]" />
<xsl:variable name="prevNodesAfterVOI"
      select="preceding-sibling::node[. >> $prevVOI]" />

Upvotes: 2

Related Questions