JPM
JPM

Reputation: 2066

XPATH for first element whose name is among the names of some other elements

<choices>
   <sic  />
   <corr />
   <reg  />
   <orig />
</choices>

<choice>
   <corr>Red</corr>
   <sic>Blue</sic>
<choice>

I want to select the first element in <choice> whose name matches the name of any element in <choices>.

If name(node-set) returned a list of names instead of only the name of the first node, I could use

select="choice/*[name() = name(choices/*)][1]"

But it doesn't (at least not in 1.0), so instead I join the names together in a string and use contains():

<xsl:variable name="choices.str">
    <xsl:for-each select="choices/*">
        <xsl:text> </xsl:text><xsl:value-of select="concat(name(),' ')"/>
    </xsl:for-each>
</xsl:variable>
<xsl:apply-templates select="choice/*[contains($choices.str,name())][1]"/>

and get what I want:

Red, the value of <corr>

Is there a more straightforward way?

Upvotes: 2

Views: 1852

Answers (2)

Sean B. Durkin
Sean B. Durkin

Reputation: 12729

You can use the key() function like this...

When this input document...

<t>
<choices>
   <sic  />
   <corr />
   <reg  />
   <orig />
</choices>
<choice>
   <corr>Red</corr>
   <sic>Blue</sic>
  </choice>
</t>

...is supplied as input to this XSLT 1.0 style-sheet...

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kChoices" match="choices/*" use="name()" />  

<xsl:template match="/">
  <xsl:variable name="first-choice" select="(*/choice/*[key('kChoices',name())])[1]" />
  <xsl:value-of select="$first-choice" />
  <xsl:text>, the value of &lt;</xsl:text>
  <xsl:value-of select="name( $first-choice)" />
  <xsl:text>&gt;</xsl:text>
</xsl:template>

</xsl:stylesheet>

...this output text is produced...

Red, the value of <corr>

XSLT 2.0 Aside

In XSLT 2.0, you would be able to use the following alternatives for the computation of the $first-choice variable...

Option 1:

(*/choice/*[for $c in . return ../../choices/*[name()=name($c)]])[1]

Option 2:

(*/choice/*[some $c in ../../choices/* satisfies name($c)=name()])[1]

Upvotes: 1

Dimitre Novatchev
Dimitre Novatchev

Reputation: 243549

I. Use this XPath 2.0 one-liner:

/*/choice/*[name() = /*/choices/*/name()][1]

When this XPath expression is evaluated against the following XML document (the provided one, but corrected to become a well-formed XML document):

<t>
    <choices>
        <sic  />
        <corr />
        <reg  />
        <orig />
    </choices>
    <choice>
        <corr>Red</corr>
        <sic>Blue</sic>
    </choice>
</t>

the correct element is selected:

<corr>Red</corr>

II. XSLT 1.0 (no 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:variable name="vNames">
  <xsl:for-each select="/*/choices/*">
   <xsl:value-of select="concat(' ', name(), ' ')"/>
  </xsl:for-each>
 </xsl:variable>

 <xsl:template match="/">
  <xsl:copy-of select=
  "/*/choice/*
         [contains($vNames, concat(' ', name(), ' '))]
           [1]"/>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied on the same XML document (above), again the correct element is selected (and copied to the output):

<corr>Red</corr>

III. 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="kChoiceByName" match="choice/*"
   use="boolean(/*/choices/*[name()=name(current())])"/>

 <xsl:template match="/">
  <xsl:copy-of select="/*/choice/*[key('kChoiceByName', true())][1]"/>
 </xsl:template>
</xsl:stylesheet>

When this transformation is applied against the same XML document (above), the same correct result is produced:

<corr>Red</corr>

It is recommended to the reader to try to understand how this all "works" :)

Upvotes: 2

Related Questions